Skill: cianbyrne1010/pctx (version 1.0.1)

--- name: pctx description: MCP aggregation and Code Mode execution layer for token-efficient agent workflows. Wraps portofcontext/pctx — connects agents to Linear, GitHub, and other MCP servers via a single local endpoint. Use when agents need to call MCP tools or batch multiple tool calls into a single TypeScript execution to reduce token cost. homepage: https://github.com/portofcontext/pctx metadata: {"clawdis":{"emoji":"🔌","requires":{"tools":["brew","npm","node"]}}} --- # pctx — MCP Aggregation & Code Mode Execution ## What is pctx? pctx is a local server that: 1. **Aggregates MCP servers** — connects to Linear, GitHub, and other MCP backends behind one endpoint 2. **Code Mode** — instead of sequential tool calls, agents write TypeScript that runs in a Deno sandbox; only the result comes back (up to 98% token reduction on complex workflows) **Live endpoint:** `http://127.0.0.1:8080/mcp` **Connected MCPs:** Linear (42 tools), GitHub (41 tools) **Config:** `~/.config/pctx/pctx.json` **Logs:** `/tmp/pctx.log` | `/tmp/pctx.err` --- ## When to use - Agent needs to call Linear or GitHub tools via MCP - Agent is doing multi-step tool workflows (pctx batches them into one TypeScript call) - Any workflow where sequential tool calls return large intermediate payloads --- ## Quick Start ```bash # Check if pctx is running {baseDir}/pctx-skill.sh status # List connected MCP servers + tool counts {baseDir}/pctx-skill.sh mcp-list # Test a tool call {baseDir}/pctx-skill.sh test linear linear_getOrganization {baseDir}/pctx-skill.sh test github list_issues ``` --- ## Commands ### Daemon Management ```bash # Status — shows port, uptime {baseDir}/pctx-skill.sh status # Start daemon (launchd) {baseDir}/pctx-skill.sh start # Stop daemon {baseDir}/pctx-skill.sh stop # Restart {baseDir}/pctx-skill.sh restart ``` ### MCP Server Management ```bash # List all connected MCPs + tool counts + connection health {baseDir}/pctx-skill.sh mcp-list # Add an upstream MCP — examples: # stdio MCP with npm package {baseDir}/pctx-skill.sh mcp-add memory --command "npx" --arg "-y" --arg "@modelcontextprotocol/server-memory" # stdio MCP with installed binary {baseDir}/pctx-skill.sh mcp-add linear --command "mcp-linear" --env "LINEAR_API_TOKEN=your_token" # HTTP MCP {baseDir}/pctx-skill.sh mcp-add stripe https://mcp.stripe.com # Remove an MCP {baseDir}/pctx-skill.sh mcp-remove <name> ``` ### Config & Backup ```bash # Backup pctx.json (always done automatically before mcp-add/remove) {baseDir}/pctx-skill.sh config-backup # Restore from backup (interactive if no timestamp given) {baseDir}/pctx-skill.sh config-restore {baseDir}/pctx-skill.sh config-restore 20260422-092733 ``` ### Testing ```bash # Test a specific MCP tool via pctx {baseDir}/pctx-skill.sh test linear linear_getOrganization {baseDir}/pctx-skill.sh test linear linear_getTeams {baseDir}/pctx-skill.sh test github list_issues {baseDir}/pctx-skill.sh test github get_file_contents ``` ### Install (idempotent) ```bash # Run on new VMs or after full rollback {baseDir}/install.sh ``` --- ## Calling pctx from Agent Code The pctx server exposes an MCP endpoint at `http://127.0.0.1:8080/mcp`. Agents can call tools directly via JSON-RPC, or use Code Mode for batched TypeScript execution. ### Simple tool call via curl ```bash # Initialize + get session context curl -s -X POST http://127.0.0.1:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}' # Call a tool (Code Mode — batched TypeScript) curl -s -X POST http://127.0.0.1:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"execute_typescript","arguments":{"script":"const org = await linear.linear_getOrganization({}); const teams = await linear.linear_getTeams({}); return { org, teams };"}}}' ``` ### Code Mode — batch multiple calls The `execute_typescript` tool is the key feature. Write one script wrapped in `async function run()` that calls all tools needed: **Important:** field name is `code` (not `script`). Code must be wrapped in `async function run() { ... }`. Namespaces: `Linear` (capital L), `Github` (capital G, no 'b'). ```typescript // BAD — multiple round-trips (traditional MCP) const org = await Linear.linearGetOrganization({}); // ... agent receives result, burns tokens ... const teams = await Linear.linearGetTeams({}); // ... agent receives result, burns tokens ... // GOOD — one Code Mode call, one result async function run() { const org = await Linear.linearGetOrganization({}); const teams = await Linear.linearGetTeams({}); const issues = await Linear.linearSearchIssues({ query: "urgent" }); return JSON.stringify({ org, teams, issues }, null, 2); } // GitHub example async function run() { const branches = await Github.listBranches({ owner: "MJM-Agents", repo: "rolling-reno-theme" }); return JSON.stringify(branches, null, 2); } ``` --- ## Currently Connected MCPs | Name | Binary | Transport | Auth | Tools | |------|--------|-----------|------|-------| | linear | mcp-linear | stdio | LINEAR_API_TOKEN | 42 | | github | github-mcp-server stdio | stdio | GITHUB_PERSONAL_ACCESS_TOKEN | 41 | --- ## Rollback Full rollback instructions: `ROLLBACK-MCP-PCTX.md` in workspace root. Quick rollback — remove one MCP: ```bash {baseDir}/pctx-skill.sh mcp-remove linear # removes Linear, leaves GitHub {baseDir}/pctx-skill.sh mcp-remove github # removes GitHub, leaves Linear ``` Full nuclear rollback: ```bash {baseDir}/pctx-skill.sh stop brew uninstall pctx github-mcp-server npm uninstall -g @tacticlaunch/mcp-linear rm -rf ~/.config/pctx/ ~/Library/LaunchAgents/ai.openclaw.pctx.plist ``` --- ## Config | Variable | Default | Description | |----------|---------|-------------| | `PCTX_CONFIG` | `~/.config/pctx/pctx.json` | Path to pctx config | | `PCTX_PORT` | `8080` | Port pctx listens on | | `PCTX_HOST` | `127.0.0.1` | Host pctx binds to | | `PCTX_BIN` | auto-detected | Path to pctx binary | --- ## Notes - pctx.json is chmod 600 — contains API keys, never commit - Deno sandbox: 10s execution timeout, no filesystem/env/system access - OAuth not yet supported in pctx (v0.7.1) — remote Linear/GitHub MCPs require OAuth; use stdio/local for now - Config auto-backed-up before every `mcp-add` / `mcp-remove` - PAT registered under `mjm-dex` GitHub account — swap if needed

path: README.md
content: # pctx Skill — Developer Notes

## Structure

```
skills/pctx/
├── SKILL.md          — User-facing docs (loaded by OpenClaw when skill is active)
├── pctx-skill.sh     — Main entrypoint (all commands)
├── install.sh        — Idempotent dependency installer
└── README.md         — This file
```

## Tickets

- MJM-209 — Research: Linear + GitHub MCP selection
- MJM-210 — Setup: pctx installed + MCPs configured
- MJM-211 — Skill build (this)
- MJM-212 — ClawHub publish

## Implementation Notes

### Why stdio, not remote MCPs?
pctx v0.7.1 doesn't support OAuth yet (open issue). Both official Linear MCP (`mcp.linear.app/sse`) and GitHub MCP (`api.githubcopilot.com/mcp/`) use OAuth. We use stdio/local variants that accept API keys.

### mcp-add design
Wraps `pctx mcp add` with:
1. Auto-backup before any config change
2. Daemon restart after add (so new server is live)

The `--arg` flag is tricky — when the command is `npx -y @package`, the `-y` trips pctx's arg parser. Use the installed binary directly instead of npx where possible (e.g. `mcp-linear` binary from npm global install, `github-mcp-server` binary from brew).

### test command
Uses pctx's `execute_typescript` Code Mode tool to call any MCP tool. The namespace matches the MCP server name (e.g. `linear.linear_getOrganization({})`).

### Token masking
All API keys shown in output are masked: first 10 + last 6 chars.

## Testing

Run QA checklist manually:

```bash
BASE=~/.openclaw/workspace/skills/pctx

# Basic smoke
bash $BASE/pctx-skill.sh status
bash $BASE/pctx-skill.sh mcp-list

# Daemon cycle
bash $BASE/pctx-skill.sh stop
bash $BASE/pctx-skill.sh status
bash $BASE/pctx-skill.sh start
bash $BASE/pctx-skill.sh status

# MCP tool test
bash $BASE/pctx-skill.sh test linear linear_getOrganization
bash $BASE/pctx-skill.sh test github list_issues

# Backup/restore
bash $BASE/pctx-skill.sh config-backup
ls ~/.config/pctx/pctx.json.backup.*
```

## ClawHub Publish (MJM-212)

Prep for ClawHub:
1. `description` field in SKILL.md frontmatter is the listing text
2. Icon: 🔌 (set in metadata.clawdis.emoji)
3. Tags: mcp, token-efficiency, code-mode, linear, github
4. Verify `{baseDir}` is used (not hardcoded paths) in SKILL.md examples
5. Test clean install via install.sh on a fresh system

## Known Limitations

- No OAuth support in pctx (v0.7.1) — blocks use of official remote MCPs
- Deno sandbox timeout: 10s hard limit — long-running TypeScript will fail
- `npx -y <package>` doesn't work with `pctx mcp add --arg` due to arg parsing — use installed binaries


path: install.sh
content: #!/usr/bin/env bash
# install.sh — Idempotent install for pctx skill dependencies
# Run once on a new VM or after a full rollback to re-establish the pctx layer.
set -euo pipefail

PCTX_CONFIG="$HOME/.config/pctx/pctx.json"
PCTX_PLIST="$HOME/Library/LaunchAgents/ai.openclaw.pctx.plist"

ok()   { echo "✅ $*"; }
info() { echo "ℹ️  $*"; }
die()  { echo "❌ $*" >&2; exit 1; }

echo "🚀 pctx skill install — idempotent"
echo ""

# 1. pctx binary
if command -v pctx &>/dev/null; then
  ok "pctx already installed: $(pctx --version 2>/dev/null)"
else
  info "Installing pctx via brew..."
  brew install portofcontext/tap/pctx
  ok "pctx installed"
fi

# 2. Deno (Code Mode sandbox runtime)
if command -v deno &>/dev/null; then
  ok "deno already installed: $(deno --version 2>/dev/null | head -1)"
else
  info "Installing deno..."
  brew install deno
  ok "deno installed"
fi

# 3. github-mcp-server
if command -v github-mcp-server &>/dev/null; then
  ok "github-mcp-server already installed: $(github-mcp-server --version 2>&1 | head -1)"
else
  info "Installing github-mcp-server..."
  brew install github-mcp-server
  ok "github-mcp-server installed"
fi

# 4. @tacticlaunch/mcp-linear
if command -v mcp-linear &>/dev/null; then
  ok "mcp-linear already installed"
else
  info "Installing @tacticlaunch/mcp-linear..."
  npm install -g @tacticlaunch/mcp-linear
  ok "mcp-linear installed"
fi

# 5. pctx config
if [[ -f "$PCTX_CONFIG" ]]; then
  ok "pctx config exists: $PCTX_CONFIG"
else
  info "Initialising pctx config..."
  mkdir -p "$HOME/.config/pctx"
  pctx mcp init -y -c "$PCTX_CONFIG"
  chmod 600 "$PCTX_CONFIG"
  ok "pctx config created"
  echo ""
  echo "⚠️  MCP servers not configured. Add them manually:"
  echo "   pctx-skill mcp-add linear --command 'mcp-linear' --env 'LINEAR_API_TOKEN=your_key'"
  echo "   pctx-skill mcp-add github --command 'github-mcp-server' --arg 'stdio' --env 'GITHUB_PERSONAL_ACCESS_TOKEN=your_pat'"
fi

# 6. launchd daemon
if [[ -f "$PCTX_PLIST" ]]; then
  ok "launchd plist exists: $PCTX_PLIST"
else
  info "⚠️  launchd plist not found at $PCTX_PLIST"
  echo "   The plist was configured during MJM-210 setup."
  echo "   If you need to recreate it, see ROLLBACK-MCP-PCTX.md for the full plist content."
fi

# 7. Start daemon if not running
if launchctl list 2>/dev/null | grep -q "ai.openclaw.pctx"; then
  ok "pctx daemon already running"
else
  if [[ -f "$PCTX_PLIST" ]]; then
    info "Loading pctx daemon..."
    launchctl load "$PCTX_PLIST"
    sleep 2
    if curl -sf --max-time 3 http://127.0.0.1:8080/mcp -o /dev/null 2>/dev/null; then
      ok "pctx daemon started on http://127.0.0.1:8080/mcp"
    else
      echo "⚠️  Daemon loaded but /mcp not yet responding. Check /tmp/pctx.err"
    fi
  fi
fi

echo ""
ok "pctx skill install complete"
echo ""
echo "Quick test:"
echo "  bash $(dirname "$0")/pctx-skill.sh status"
echo "  bash $(dirname "$0")/pctx-skill.sh mcp-list"


path: pctx-skill.sh
content: #!/usr/bin/env bash
# pctx-skill.sh — OpenClaw skill wrapper for pctx MCP aggregation layer
# Usage: pctx-skill.sh <command> [args]
set -euo pipefail

PCTX_CONFIG="${PCTX_CONFIG:-$HOME/.config/pctx/pctx.json}"
PCTX_PLIST="$HOME/Library/LaunchAgents/ai.openclaw.pctx.plist"
PCTX_PORT="${PCTX_PORT:-8080}"
PCTX_HOST="${PCTX_HOST:-127.0.0.1}"
PCTX_BIN="${PCTX_BIN:-$(which pctx 2>/dev/null || echo '')}"

# ─── Helpers ──────────────────────────────────────────────────────────────────

die() { echo "❌ $*" >&2; exit 1; }
ok()  { echo "✅ $*"; }
info(){ echo "ℹ️  $*"; }

require_pctx() {
  [[ -n "$PCTX_BIN" ]] || die "pctx not found. Install with: brew install portofcontext/tap/pctx"
  [[ -f "$PCTX_CONFIG" ]] || die "pctx config not found at $PCTX_CONFIG. Run: pctx mcp init -c $PCTX_CONFIG"
}

mask_token() {
  local tok="$1"
  if [[ ${#tok} -gt 20 ]]; then
    echo "${tok:0:10}***${tok: -6}"
  else
    echo "***"
  fi
}

daemon_running() {
  # Check launchd has a running PID (not just loaded)
  local pid
  pid=$(launchctl list 2>/dev/null | grep ai.openclaw.pctx | awk '{print $1}')
  [[ -z "$pid" || "$pid" == "-" ]] && return 1
  # Verify /mcp endpoint is responding (requires both Accept types)
  curl -sf --max-time 3 \
    -X POST "http://${PCTX_HOST}:${PCTX_PORT}/mcp" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"healthcheck","version":"1"}}}' \
    -o /dev/null 2>/dev/null
}

wait_for_mcp() {
  local max=20 i=0
  while ! curl -sf --max-time 3 \
    -X POST "http://${PCTX_HOST}:${PCTX_PORT}/mcp" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"healthcheck","version":"1"}}}' \
    -o /dev/null 2>/dev/null; do
    ((i++))
    [[ $i -ge $max ]] && die "pctx didn't respond after ${max}s. Check /tmp/pctx.err"
    sleep 1
    echo -n "."
  done
  echo ""
}

# ─── Commands ─────────────────────────────────────────────────────────────────

cmd_status() {
  if daemon_running; then
    local pid
    pid=$(launchctl list 2>/dev/null | grep ai.openclaw.pctx | awk '{print $1}')
    local start_time=""
    if [[ -n "$pid" && "$pid" != "-" ]]; then
      start_time=$(ps -p "$pid" -o lstart= 2>/dev/null | xargs || echo "")
    fi
    echo "🟢 RUNNING — http://${PCTX_HOST}:${PCTX_PORT}/mcp"
    [[ -n "$start_time" ]] && echo "   Started: $start_time"
    echo "   Config:  $PCTX_CONFIG"
    echo "   Logs:    /tmp/pctx.log | /tmp/pctx.err"
  else
    echo "🔴 STOPPED"
    echo "   Run: pctx-skill start"
  fi
}

cmd_start() {
  require_pctx
  if daemon_running; then
    ok "pctx already running on http://${PCTX_HOST}:${PCTX_PORT}/mcp"
    return 0
  fi
  [[ ! -f "$PCTX_PLIST" ]] && die "launchd plist not found at $PCTX_PLIST. Was MJM-210 setup completed?"
  info "Starting pctx daemon..."
  launchctl load "$PCTX_PLIST" 2>&1 || true
  sleep 2
  echo -n "Waiting for /mcp endpoint"
  wait_for_mcp
  ok "pctx running on http://${PCTX_HOST}:${PCTX_PORT}/mcp"
  return 0
}

cmd_stop() {
  if ! launchctl list 2>/dev/null | grep -q "ai.openclaw.pctx"; then
    info "pctx daemon not loaded"
    return 0
  fi
  info "Stopping pctx daemon..."
  launchctl unload "$PCTX_PLIST" 2>/dev/null || true
  pkill -f "pctx mcp start" 2>/dev/null || true
  sleep 1
  ok "pctx stopped"
  return 0
}

cmd_restart() {
  # Full stop: unload launchd + kill any orphan processes
  launchctl unload "$PCTX_PLIST" 2>/dev/null || true
  pkill -f "pctx mcp start" 2>/dev/null || true
  sleep 2
  ok "pctx stopped"
  cmd_start
}

cmd_mcp_list() {
  require_pctx
  "$PCTX_BIN" mcp list -c "$PCTX_CONFIG" 2>&1
}

cmd_mcp_add() {
  require_pctx
  local name="${1:-}"
  [[ -n "$name" ]] || die "Usage: pctx-skill mcp-add <name> [--url <url> | --command <cmd>] [--arg arg ...] [--env KEY=VAL ...]"
  shift

  # Build pctx mcp add args, pass through remaining flags
  cmd_config_backup
  "$PCTX_BIN" mcp add "$name" -c "$PCTX_CONFIG" "$@" && ok "MCP '$name' added" || die "Failed to add MCP '$name'"

  if daemon_running; then
    info "Restarting daemon to pick up new MCP..."
    cmd_restart
  fi
}

cmd_mcp_remove() {
  require_pctx
  local name="${1:-}"
  [[ -n "$name" ]] || die "Usage: pctx-skill mcp-remove <name>"
  cmd_config_backup
  "$PCTX_BIN" mcp remove "$name" -c "$PCTX_CONFIG" 2>&1 && ok "MCP '$name' removed"
  if daemon_running; then
    info "Restarting daemon..."
    cmd_restart
  fi
}

cmd_config_backup() {
  local ts
  ts=$(date +%Y%m%d-%H%M%S)
  local dest="${PCTX_CONFIG}.backup.${ts}"
  cp "$PCTX_CONFIG" "$dest"
  chmod 600 "$dest"
  ok "Backup: $dest"
}

cmd_config_restore() {
  local ts="${1:-}"
  local backups=()
  while IFS= read -r f; do backups+=("$f"); done < <(ls "${PCTX_CONFIG}.backup."* 2>/dev/null | sort -r)

  [[ ${#backups[@]} -gt 0 ]] || die "No backups found at ${PCTX_CONFIG}.backup.*"

  local chosen=""
  if [[ -n "$ts" ]]; then
    chosen="${PCTX_CONFIG}.backup.${ts}"
    [[ ! -f "$chosen" ]] && die "Backup not found: $chosen"
  else
    echo "Available backups:"
    for i in "${!backups[@]}"; do
      echo "  [$i] ${backups[$i]}"
    done
    echo -n "Enter number to restore: "
    read -r idx
    chosen="${backups[$idx]}"
  fi

  info "Restoring from: $chosen"
  cp "$chosen" "$PCTX_CONFIG"
  chmod 600 "$PCTX_CONFIG"
  ok "Config restored from $chosen"

  if daemon_running; then
    info "Restarting daemon..."
    cmd_restart
  fi
}

cmd_test() {
  local server="${1:-}"
  local fn="${2:-}"
  [[ -n "$server" ]] || die "Usage: pctx-skill test <linear|github> [function-name]"

  daemon_running || die "pctx daemon not running. Run: pctx-skill start"

  # Resolve namespace capitalisation
  local ns
  case "$server" in
    linear) ns="Linear" ;;
    github) ns="Github" ;;
    *) ns="$server" ;;
  esac

  info "Testing pctx Code Mode for '$ns'..."

  if [[ -z "$fn" ]]; then
    # No function specified — list available functions for this namespace
    # Use a broad query and filter by namespace in post-processing
    local list_resp
    list_resp=$(curl -sf --max-time 15 -X POST "http://${PCTX_HOST}:${PCTX_PORT}/mcp" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_functions","arguments":{"query":"*","limit":100}}}' \
      2>/dev/null | grep "^data:" | head -1 | sed 's/^data: //')
    echo "📚 Available functions in $ns:"
    echo "$list_resp" | python3 -c "
import sys, json, re
ns = '$ns'
d = json.load(sys.stdin)
text = d.get('result',{}).get('content',[{}])[0].get('text','')
# Find the specific namespace block
pattern = f'namespace {ns}'
idx = text.find(pattern)
if idx >= 0:
    block = text[idx:idx+5000]
    # Extract until next namespace or end
    next_ns = re.search(r'namespace \w+', block[len(pattern):])
    if next_ns:
        block = block[:len(pattern) + next_ns.start()]
    fns = re.findall(r'export async function (\w+)', block)
    for f in fns[:30]: print('  -', f)
else:
    print(f'  Namespace {ns} not found in pctx catalog')
" 2>/dev/null
    return
  fi

  # Function specified — call it via execute_typescript
  # pctx requires: field="code", wrapped in "async function run() { ... }"
  info "Calling ${ns}.${fn}({}) via execute_typescript..."
  local code="async function run() { const result = await ${ns}.${fn}({}); return JSON.stringify(result, null, 2); }"
  local call_payload
  call_payload=$(python3 -c "
import json
print(json.dumps({'jsonrpc':'2.0','id':2,'method':'tools/call','params':{
  'name':'execute_typescript',
  'arguments':{'code':'$code'}
}}))
" 2>/dev/null)

  local call_resp
  call_resp=$(curl -sf --max-time 30 -X POST "http://${PCTX_HOST}:${PCTX_PORT}/mcp" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -d "$call_payload" \
    2>/dev/null | grep "^data:" | tail -1 | sed 's/^data: //')

  if [[ -z "$call_resp" ]]; then
    die "No response from pctx. Check /tmp/pctx.log"
  fi

  echo "📦 Result:"
  echo "$call_resp" | python3 -c "
import sys, json
d = json.load(sys.stdin)
if 'result' in d:
    content = d.get('result',{}).get('content',[])
    for c in content:
        txt = c.get('text','')
        print(txt[:800] + ('...' if len(txt) > 800 else ''))
elif 'error' in d:
    print('❌ Error:', json.dumps(d['error'], indent=2))
else:
    print(json.dumps(d, indent=2)[:800])
" 2>/dev/null || echo "$call_resp" | head -c 800
}

cmd_install() {
  info "Checking pctx installation..."

  # pctx
  if ! command -v pctx &>/dev/null; then
    info "Installing pctx..."
    brew install portofcontext/tap/pctx
  else
    ok "pctx $(pctx --version 2>/dev/null)"
  fi

  # Deno
  if ! command -v deno &>/dev/null; then
    info "Installing deno..."
    brew install deno
  else
    ok "deno $(deno --version 2>/dev/null | head -1)"
  fi

  # github-mcp-server
  if ! command -v github-mcp-server &>/dev/null; then
    info "Installing github-mcp-server..."
    brew install github-mcp-server
  else
    ok "github-mcp-server $(github-mcp-server --version 2>&1 | head -1)"
  fi

  # @tacticlaunch/mcp-linear
  if ! command -v mcp-linear &>/dev/null; then
    info "Installing @tacticlaunch/mcp-linear..."
    npm install -g @tacticlaunch/mcp-linear
  else
    ok "mcp-linear installed"
  fi

  ok "All dependencies installed"
}

# ─── Dispatch ─────────────────────────────────────────────────────────────────

CMD="${1:-help}"
shift 2>/dev/null || true

case "$CMD" in
  status)          cmd_status ;;
  start)           cmd_start "$@" ;;
  stop)            cmd_stop ;;
  restart)         cmd_restart ;;
  mcp-list)        cmd_mcp_list ;;
  mcp-add)         cmd_mcp_add "$@" ;;
  mcp-remove)      cmd_mcp_remove "$@" ;;
  config-backup)   cmd_config_backup ;;
  config-restore)  cmd_config_restore "$@" ;;
  test)            cmd_test "$@" ;;
  install)         cmd_install ;;
  help|--help|-h)
    cat <<'EOF'
pctx-skill — OpenClaw skill for pctx MCP aggregation

Usage: pctx-skill <command> [args]

Commands:
  status                        Check daemon status + port
  start                         Start pctx daemon (launchd)
  stop                          Stop pctx daemon
  restart                       Restart pctx daemon
  mcp-list                      List connected MCP servers + tool counts
  mcp-add <name> [flags]        Add an upstream MCP server
  mcp-remove <name>             Remove an upstream MCP server
  config-backup                 Backup pctx.json (timestamped)
  config-restore [timestamp]    Restore pctx.json from backup
  test <server> <tool>          Test a tool call through pctx
  install                       Idempotent install of all dependencies

Examples:
  pctx-skill status
  pctx-skill mcp-list
  pctx-skill test linear linear_getOrganization
  pctx-skill test github list_issues
  pctx-skill mcp-add memory --command "npx -y @modelcontextprotocol/server-memory"
  pctx-skill config-backup
  pctx-skill config-restore 20260422-092733

Config: ~/.config/pctx/pctx.json
Logs:   /tmp/pctx.log | /tmp/pctx.err
EOF
    ;;
  *)
    die "Unknown command: $CMD. Run 'pctx-skill help' for usage."
    ;;
esac


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)