Skill: loonghao/dcc-cli-gateway (version 0.17.47)

--- name: dcc-cli-gateway description: >- Control live DCC hosts (Maya, Blender, Houdini, Photoshop, 3ds Max, and custom studio tools) through the dcc-mcp-cli command line. For ClawHub, OpenClaw, Cursor, Claude, and shell-capable agent hosts: verify gateway health, inventory registered DCC instances, search tools, inspect schemas, and invoke tools without speaking MCP directly. If dcc-mcp-cli is missing, ask for consent, download it from GitHub Releases, and fall back to Python stdlib REST only if download fails. license: MIT-0 compatibility: Cross-platform Windows/macOS/Linux. Prefers dcc-mcp-cli on PATH; can download release asset from GitHub; Python 3.7+ stdlib REST fallback. DCC-MCP gateway reachable at DCC_MCP_BASE_URL (default http://127.0.0.1:9765) allowed-tools: Bash Read metadata: dcc-mcp: dcc: python layer: infrastructure version: "0.17.47" # x-release-please-version search-hint: "cli gateway dcc-mcp-cli connect dcc instances search describe call clawhub" tags: "cli, gateway, infrastructure, clawhub, openclaw, instances" openclaw: requires: env: - DCC_MCP_BASE_URL primaryEnv: DCC_MCP_BASE_URL emoji: "🖥️" homepage: https://github.com/loonghao/dcc-mcp-core/blob/main/skills/dcc-cli-gateway/SKILL.md --- # DCC CLI Gateway — Agent Control Plane Use this skill when an agent host can run shell commands and should connect to DCC-MCP through **`dcc-mcp-cli`** instead of MCP JSON-RPC. The CLI wraps the gateway REST API and returns JSON by default. The bundled Python fallback sends `Accept: application/json` because the gateway REST API itself now defaults to compact TOON for agent-facing routes. Connection order: 1. Use `dcc-mcp-cli` when it is already on `PATH`. 2. If missing, ask user permission, then download `dcc-mcp-cli` from GitHub Releases. 3. If the download fails, use the bundled Python stdlib REST fallback. Install via OpenClaw/ClawHub, or point your agent at this `SKILL.md` after cloning [[REDACTED_SECRET]](https://github.com/loonghao/dcc-mcp-core/tree/main/skills/dcc-cli-gateway). --- ## Critical Rules | Situation | You MUST | |-----------|----------| | Starting any DCC task | Run `python scripts/dcc_gateway.py health` and `python scripts/dcc_gateway.py list` first | | `dcc-mcp-cli` missing | Ask permission before `--ensure-cli`; fallback Python REST is allowed if download fails | | Inventory returns `total == 0` | Stop; do not run `search`, `describe`, or `call` | | Gateway unreachable | Stop; explain; ask user permission before troubleshooting | | User has not agreed to setup | Do not install packages, edit env files, launch GUI apps, or write configs | | User approved setup | Follow [`references/ZERO_INSTANCES_CLI.md`](references/ZERO_INSTANCES_CLI.md) | | After DCC crash/restart | Re-run `list` and `search`; old slugs may be invalid | --- ## Configuration `dcc-mcp-cli` and the Python helper read the gateway URL from `DCC_MCP_BASE_URL`. ```bash export DCC_MCP_BASE_URL="${DCC_MCP_BASE_URL:-http://127.0.0.1:9765}" dcc-mcp-cli health python scripts/dcc_gateway.py health ``` For a one-off command: ```bash python scripts/dcc_gateway.py --base-url http://127.0.0.1:9765 health ``` Quick probe helper: ```bash python3 scripts/check_cli.py py -3 scripts\check_cli.py ``` Flags: `--base-url URL`, `--cli dcc-mcp-cli`, `--ensure-cli`, `--install-dir DIR`, `--pretty`. When the user approves downloading the CLI: ```bash # Linux / macOS python3 scripts/dcc_gateway.py --ensure-cli list vx python scripts/dcc_gateway.py --ensure-cli list # Windows py -3 scripts\dcc_gateway.py --ensure-cli list vx python scripts\dcc_gateway.py --ensure-cli list ``` Release assets are selected by platform: | Platform | Asset | |----------|-------| | Windows x86_64 | `dcc-mcp-cli-windows-x86_64.exe` | | Linux x86_64 | `dcc-mcp-cli-linux-x86_64` | | macOS Intel/Apple Silicon | `dcc-mcp-cli-macos-universal2` | If Python is not easy to locate, install vx first and run the helper through `vx python`: ```bash # Linux / macOS curl -fsSL https://raw.githubusercontent.com/loonghao/vx/main/install.sh | bash # Windows PowerShell powershell -c "irm https://raw.githubusercontent.com/loonghao/vx/main/install.ps1 | iex" ``` --- ## Step 0 — Mandatory Instance Inventory Run this every time you begin work or after the user starts/stops a DCC host: ```bash python scripts/dcc_gateway.py health python scripts/dcc_gateway.py list ``` Interpret `dcc-mcp-cli list`: ```json { "total": 1, "instances": [ { "instance_id": "full-uuid", "instance_short": "a1b2c3d4", "dcc_type": "maya", "status": "available", "stale": false, "mcp_url": "http://127.0.0.1:8765/mcp" } ] } ``` Report to the user: 1. `total` 2. Count by `dcc_type` 3. Any `stale: true` rows 4. The target `instance_id` or `instance_short` you will use If `total == 0`, stop and ask whether the user wants setup guidance for the target DCC. Continue only after explicit approval. --- ## Step 1 — Search Tools Only run this when inventory shows at least one non-stale target: ```bash python scripts/dcc_gateway.py search --query sphere --dcc-type maya --limit 20 ``` Copy the returned slug exactly. Gateway slugs look like: ```text maya.a1b2c3d4.maya_primitives__create_sphere ``` Never hand-build slugs. --- ## Step 2 — Describe Schema ```bash python scripts/dcc_gateway.py describe maya.a1b2c3d4.maya_primitives__create_sphere ``` Read `tool.inputSchema` and safety annotations before calling. --- ## Step 3 — Call a Tool ```bash python scripts/dcc_gateway.py call maya.a1b2c3d4.maya_primitives__create_sphere \ --json '{"radius":2.0}' ``` Tool-specific fields (`code`, `file_path`, `radius`, and similar) belong inside the `--json` object. Do not pass them as top-level CLI flags unless the CLI adds an explicit first-class flag later. See [`references/CLI_CHEATSHEET.md`](references/CLI_CHEATSHEET.md) for command patterns and common errors. --- ## What This Skill Does Not Use - MCP `tools/list`, `tools/call`, or `resources/read` - Raw `curl` workflows except when debugging the gateway itself - Direct Maya/Blender/Houdini scripting The CLI is the preferred agent-facing control plane. The Python fallback uses the same gateway REST endpoints only when the CLI is unavailable after a download attempt fails.

path: references/CLI_CHEATSHEET.md
content: # CLI cheatsheet — DCC-MCP gateway

Base URL: `$DCC_MCP_BASE_URL` (default `http://127.0.0.1:9765`).

Preferred helper:

```bash
python scripts/dcc_gateway.py <command>
vx python scripts/dcc_gateway.py <command>
```

It uses `dcc-mcp-cli` when available. With user consent, add `--ensure-cli` to
download the CLI from GitHub Releases when missing. If download fails, it falls
back to Python stdlib REST.

If Python is not available as `python` / `py`, install vx and use
`vx python scripts/dcc_gateway.py ...`:

```bash
# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/loonghao/vx/main/install.sh | bash

# Windows PowerShell
powershell -c "irm https://raw.githubusercontent.com/loonghao/vx/main/install.ps1 | iex"
```

## Discovery and health

| Command | Purpose |
|---------|---------|
| `python scripts/dcc_gateway.py health` | Check gateway liveness |
| `python scripts/dcc_gateway.py list` | List registered DCC instances |
| `python scripts/dcc_gateway.py --pretty list` | Human-readable JSON |

## Capability workflow

| Command | Purpose |
|---------|---------|
| `python scripts/dcc_gateway.py search --query sphere --dcc-type maya --limit 20` | Find tools |
| `python scripts/dcc_gateway.py describe <tool_slug>` | Inspect schema |
| `python scripts/dcc_gateway.py call <tool_slug> --json '{"radius":2}'` | Invoke one tool |

## Example: inventory

```bash
export DCC_MCP_BASE_URL="${DCC_MCP_BASE_URL:-http://127.0.0.1:9765}"
python scripts/dcc_gateway.py health
python scripts/dcc_gateway.py list
```

## Example: search

```bash
python scripts/dcc_gateway.py search --query sphere --dcc-type maya --limit 10
```

## Example: describe

```bash
python scripts/dcc_gateway.py describe maya.a1b2c3d4.maya_primitives__create_sphere
```

## Example: call

```bash
python scripts/dcc_gateway.py call maya.a1b2c3d4.maya_primitives__create_sphere \
  --json '{"radius":2.0}'
```

## Slug rules

- Gateway slugs are returned by `search`.
- Do not invent slugs from DCC names or tool names.
- Re-run `list` and `search` after a DCC restart.

## Common errors

| Symptom | Action |
|---------|--------|
| CLI not found | Ask user permission, then run `vx python scripts/dcc_gateway.py --ensure-cli list`; fallback Python REST runs if download fails |
| Gateway health fails | Ask before setup; do not install or launch apps silently |
| `total == 0` | Start a DCC adapter, then re-run `python scripts/dcc_gateway.py list` |
| `unknown-slug` | Re-run `search`; the instance may have restarted |
| `invalid-params` | Fix the JSON object per `describe` output |


path: references/ZERO_INSTANCES_CLI.md
content: # Zero instances — CLI setup guide

Use this document only when:

- `python scripts/dcc_gateway.py health` succeeds,
- `python scripts/dcc_gateway.py list` returns `"total": 0`, and
- the user has explicitly approved setup guidance.

Until all three are true, do not run install commands, edit environment files,
launch GUI applications, or modify MCP host configuration.

---

## User consent

Before any setup step, confirm:

1. Which DCC product the user needs.
2. Whether they want commands suggested or executed.
3. That they will confirm after each DCC-side step so you can re-run
   `python scripts/dcc_gateway.py list`.

---

## Diagnose

| Check | Meaning | Next step |
|-------|---------|-----------|
| `python scripts/dcc_gateway.py health` fails | Gateway is not reachable | Ask user to start a gateway-capable DCC adapter or `dcc-mcp-server` |
| `python scripts/dcc_gateway.py health` succeeds and `list.total == 0` | Gateway is up, no DCC registered | Start a DCC adapter |

Gateway election defaults to port `9765`. The first DCC-MCP process that binds
the gateway port becomes the gateway; other DCC adapters register with it.

---

## Adapter discovery

With user approval:

```bash
dcc-mcp-cli install --dcc-type maya
dcc-mcp-cli install --dcc-type blender
```

The `install` command returns an auditable plan. Treat it as guidance unless the
user explicitly asks you to execute installation steps.

---

## Per-DCC checklist

### Maya

1. Install into Maya's Python: `mayapy -m pip install dcc-mcp-maya`
2. In Maya Script Editor:

   ```python
   import dcc_mcp_maya
   handle = dcc_mcp_maya.start_server(port=8765)
   print(handle.mcp_url())
   ```

3. Re-run `python scripts/dcc_gateway.py list`; expect `dcc_type: maya`.

### Blender

1. Install `dcc-mcp-blender` per its README.
2. Enable the add-on in Blender Preferences.
3. Re-run `python scripts/dcc_gateway.py list`; expect `dcc_type: blender`.

### Houdini / Photoshop / 3ds Max

Follow the adapter README for the target host, then re-run:

```bash
python scripts/dcc_gateway.py list
```

When `total >= 1`, resume the main flow: `search -> describe -> call`.

If Python is not available as `python` / `py`, install vx first:

```bash
# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/loonghao/vx/main/install.sh | bash

# Windows PowerShell
powershell -c "irm https://raw.githubusercontent.com/loonghao/vx/main/install.ps1 | iex"
```


path: scripts/check_cli.py
content: """Probe dcc-mcp-cli availability and gateway instance inventory."""

from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
from typing import Any

import dcc_gateway

DEFAULT_BASE_URL = "http://127.0.0.1:9765"


def _run_json(argv: list[str]) -> tuple[bool, dict[str, Any]]:
    try:
        proc = subprocess.run(
            argv,
            capture_output=True,
            text=True,
            timeout=30,
            check=False,
        )
    except OSError as exc:
        return False, {"error": str(exc)}
    except subprocess.TimeoutExpired:
        return False, {"error": "command timed out"}

    if proc.returncode != 0:
        return False, {"returncode": proc.returncode, "stderr": proc.stderr.strip()}

    try:
        payload = json.loads(proc.stdout or "{}")
    except json.JSONDecodeError as exc:
        return False, {"error": f"invalid JSON output: {exc}", "stdout": proc.stdout}
    return True, payload


def probe(
    cli: str = "dcc-mcp-cli",
    base_url: str | None = None,
    *,
    ensure_cli: bool = False,
    install_dir: str | None = None,
) -> dict[str, Any]:
    """Return CLI availability, health, and instance inventory."""
    resolved = shutil.which(cli)
    url = base_url or os.environ.get("DCC_MCP_BASE_URL") or DEFAULT_BASE_URL
    result: dict[str, Any] = {
        "cli": cli,
        "cli_path": resolved,
        "base_url": url,
        "cli_ok": resolved is not None,
        "gateway_ok": False,
        "total": 0,
        "by_dcc_type": {},
    }
    if resolved is None and ensure_cli:
        default_install_dir = Path.home() / ".local" / "bin"
        resolved_install_dir = install_dir or os.environ.get("DCC_MCP_INSTALL_DIR") or str(default_install_dir)
        ok, message, download_url = dcc_gateway.install_cli(
            install_dir=Path(resolved_install_dir),
            repo=os.environ.get("DCC_MCP_REPO") or dcc_gateway.DEFAULT_REPO,
            version=os.environ.get("DCC_MCP_VERSION") or dcc_gateway.DEFAULT_VERSION,
        )
        result["install_attempted"] = True
        result["install_ok"] = ok
        result["install_message"] = message
        result["download_url"] = download_url
        if ok:
            resolved = message
            result["cli_path"] = resolved
            result["cli_ok"] = True
    if resolved is None:
        fallback = dcc_gateway.python_fallback(
            "list",
            argparse.Namespace(base_url=url),
        )
        result["fallback"] = "python-stdlib-rest"
        if isinstance(fallback, dict) and "instances" in fallback:
            instances = fallback.get("instances") or []
            counts: dict[str, int] = {}
            for item in instances:
                if isinstance(item, dict):
                    dcc_type = str(item.get("dcc_type") or "unknown")
                    counts[dcc_type] = counts.get(dcc_type, 0) + 1
            result["gateway_ok"] = True
            result["total"] = int(fallback.get("total") or len(instances))
            result["by_dcc_type"] = counts
            result["list"] = fallback
        return result

    health_ok, health = _run_json([resolved, "--base-url", url, "health"])
    result["health"] = health
    result["gateway_ok"] = health_ok
    if not health_ok:
        return result

    list_ok, listing = _run_json([resolved, "--base-url", url, "list"])
    result["list"] = listing
    if not list_ok:
        return result

    instances = listing.get("instances") if isinstance(listing, dict) else None
    if not isinstance(instances, list):
        instances = []
    counts: dict[str, int] = {}
    for item in instances:
        if isinstance(item, dict):
            dcc_type = str(item.get("dcc_type") or "unknown")
            counts[dcc_type] = counts.get(dcc_type, 0) + 1
    result["total"] = int(listing.get("total") or len(instances))
    result["by_dcc_type"] = counts
    return result


def main() -> int:
    """CLI entry point."""
    parser = argparse.ArgumentParser(description="Probe dcc-mcp-cli and gateway inventory")
    parser.add_argument("--cli", default="dcc-mcp-cli")
    parser.add_argument("--base-url", default=os.environ.get("DCC_MCP_BASE_URL") or DEFAULT_BASE_URL)
    parser.add_argument("--ensure-cli", action="store_true")
    parser.add_argument("--install-dir", default=os.environ.get("DCC_MCP_INSTALL_DIR"))
    parser.add_argument("--pretty", action="store_true")
    args = parser.parse_args()

    payload = probe(
        cli=args.cli,
        base_url=args.base_url,
        ensure_cli=args.ensure_cli,
        install_dir=args.install_dir,
    )
    print(json.dumps(payload, indent=2 if args.pretty else None, sort_keys=args.pretty))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/dcc_gateway.py
content: """Cross-platform gateway helper with CLI download and Python REST fallback."""

from __future__ import annotations

import argparse
import contextlib
import json
import os
from pathlib import Path
import platform
import shutil
import stat
import subprocess
import tempfile
from typing import Any
import urllib.error
import urllib.request

DEFAULT_BASE_URL = "http://127.0.0.1:9765"
DEFAULT_REPO = "loonghao/dcc-mcp-core"
DEFAULT_VERSION = "latest"


def _json_dumps(payload: Any, *, pretty: bool = False) -> str:
    return json.dumps(payload, indent=2 if pretty else None, sort_keys=pretty)


def _run_json(argv: list[str]) -> tuple[bool, dict[str, Any]]:
    try:
        proc = subprocess.run(
            argv,
            capture_output=True,
            text=True,
            timeout=60,
            check=False,
        )
    except OSError as exc:
        return False, {"error": str(exc)}
    except subprocess.TimeoutExpired:
        return False, {"error": "command timed out"}

    if proc.returncode != 0:
        return False, {"returncode": proc.returncode, "stderr": proc.stderr.strip()}

    try:
        payload = json.loads(proc.stdout or "{}")
    except json.JSONDecodeError as exc:
        return False, {"error": f"invalid JSON output: {exc}", "stdout": proc.stdout}
    return True, payload


def _request_json(base_url: str, method: str, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
    url = f"{base_url.rstrip('/')}{path}"
    data = None if body is None else json.dumps(body).encode("utf-8")
    request = urllib.request.Request(url, data=data, method=method)
    request.add_header("Accept", "application/json")
    if body is not None:
        request.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(request, timeout=60) as response:
            text = response.read().decode("utf-8")
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")
        return {"success": False, "error": "http-error", "status": exc.code, "detail": detail}
    except (urllib.error.URLError, OSError) as exc:
        return {"success": False, "error": "connection-error", "detail": str(exc)}
    if not text:
        return {}
    try:
        return json.loads(text)
    except json.JSONDecodeError as exc:
        return {"success": False, "error": "invalid-json", "detail": str(exc), "body": text}


def _asset_name() -> str | None:
    system = platform.system().lower()
    machine = platform.machine().lower()
    if system == "windows" and machine in {"amd64", "x86_64"}:
        return "dcc-mcp-cli-windows-x86_64.exe"
    if system == "linux" and machine in {"amd64", "x86_64"}:
        return "dcc-mcp-cli-linux-x86_64"
    if system == "darwin":
        return "dcc-mcp-cli-macos-universal2"
    return None


def _download_url(repo: str, version: str, asset: str) -> str:
    if version == "latest":
        return f"https://github.com/{repo}/releases/latest/download/{asset}"
    return f"https://github.com/{repo}/releases/download/{version}/{asset}"


def install_cli(
    *,
    install_dir: Path,
    repo: str = DEFAULT_REPO,
    version: str = DEFAULT_VERSION,
) -> tuple[bool, str, str | None]:
    """Download dcc-mcp-cli for the current platform."""
    asset = _asset_name()
    if asset is None:
        return False, "unsupported platform for release asset", None

    install_dir.mkdir(parents=True, exist_ok=True)
    executable_name = "dcc-mcp-cli.exe" if platform.system().lower() == "windows" else "dcc-mcp-cli"
    target = install_dir / executable_name
    url = _download_url(repo, version, asset)

    fd, tmp_name = tempfile.mkstemp(prefix="dcc-mcp-cli-", suffix=target.suffix)
    os.close(fd)
    tmp_path = Path(tmp_name)
    try:
        urllib.request.urlretrieve(url, tmp_path)
        if platform.system().lower() != "windows":
            tmp_path.chmod(tmp_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
        shutil.move(str(tmp_path), str(target))
    except (urllib.error.URLError, OSError) as exc:
        with contextlib.suppress(OSError):
            tmp_path.unlink(missing_ok=True)
        return False, f"download failed: {exc}", url
    return True, str(target), url


def resolve_cli(args: argparse.Namespace) -> tuple[str | None, dict[str, Any]]:
    """Find the CLI; optionally install it from GitHub releases."""
    found = shutil.which(args.cli)
    details: dict[str, Any] = {"cli": args.cli, "cli_path": found, "installed": False}
    if found:
        return found, details

    if not args.ensure_cli:
        return None, details

    install_dir = Path(args.install_dir).expanduser()
    ok, message, url = install_cli(install_dir=install_dir, repo=args.repo, version=args.version)
    details.update({"install_attempted": True, "install_ok": ok, "install_message": message, "download_url": url})
    if not ok:
        return None, details
    details["installed"] = True
    return message, details


def cli_args_for(command: str, args: argparse.Namespace) -> list[str]:
    """Build dcc-mcp-cli argv for a command."""
    argv = [args.cli_path, "--base-url", args.base_url, command]
    if command == "search":
        if args.query:
            argv.extend(["--query", args.query])
        if args.dcc_type:
            argv.extend(["--dcc-type", args.dcc_type])
        if args.limit is not None:
            argv.extend(["--limit", str(args.limit)])
    elif command == "describe":
        argv.append(args.tool_slug)
    elif command == "call":
        argv.extend([args.tool_slug, "--json", args.json])
        if args.meta_json:
            argv.extend(["--meta-json", args.meta_json])
    return argv


def python_fallback(command: str, args: argparse.Namespace) -> dict[str, Any]:
    """Execute the gateway workflow via Python stdlib REST calls."""
    if command == "health":
        return _request_json(args.base_url, "GET", "/v1/healthz")
    if command == "list":
        return _request_json(args.base_url, "GET", "/v1/instances")
    if command == "search":
        body: dict[str, Any] = {}
        if args.query:
            body["query"] = args.query
        if args.dcc_type:
            body["dcc_type"] = args.dcc_type
        if args.limit is not None:
            body["limit"] = args.limit
        return _request_json(args.base_url, "POST", "/v1/search", body)
    if command == "describe":
        return _request_json(
            args.base_url,
            "POST",
            "/v1/describe",
            {"tool_slug": args.tool_slug, "include_schema": True},
        )
    if command == "call":
        try:
            arguments = json.loads(args.json)
        except json.JSONDecodeError as exc:
            return {"success": False, "error": "--json must be valid JSON", "detail": str(exc)}
        body = {"tool_slug": args.tool_slug, "arguments": arguments}
        if args.meta_json:
            try:
                body["meta"] = json.loads(args.meta_json)
            except json.JSONDecodeError as exc:
                return {"success": False, "error": "--meta-json must be valid JSON", "detail": str(exc)}
        return _request_json(args.base_url, "POST", "/v1/call", body)
    raise ValueError(f"unsupported command: {command}")


def run_command(command: str, args: argparse.Namespace) -> dict[str, Any]:
    """Prefer dcc-mcp-cli, optionally install it, then fall back to Python REST."""
    cli_path, cli_details = resolve_cli(args)
    if cli_path:
        args.cli_path = cli_path
        ok, payload = _run_json(cli_args_for(command, args))
        if ok:
            return payload
        cli_details["cli_error"] = payload

    fallback_payload = python_fallback(command, args)
    if isinstance(fallback_payload, dict):
        fallback_payload.setdefault("_transport", "python-stdlib-rest")
        fallback_payload.setdefault("_cli", cli_details)
    return fallback_payload


def build_parser() -> argparse.ArgumentParser:
    """Create the helper CLI parser."""
    parser = argparse.ArgumentParser(description="DCC-MCP gateway helper with CLI-first execution.")
    parser.add_argument("--base-url", default=os.environ.get("DCC_MCP_BASE_URL") or DEFAULT_BASE_URL)
    parser.add_argument("--cli", default="dcc-mcp-cli")
    parser.add_argument("--ensure-cli", action="store_true", help="Download dcc-mcp-cli from GitHub if it is missing")
    parser.add_argument(
        "--install-dir",
        default=os.environ.get("DCC_MCP_INSTALL_DIR") or str(Path.home() / ".local" / "bin"),
    )
    parser.add_argument("--repo", default=os.environ.get("DCC_MCP_REPO") or DEFAULT_REPO)
    parser.add_argument("--version", default=os.environ.get("DCC_MCP_VERSION") or DEFAULT_VERSION)
    parser.add_argument("--pretty", action="store_true")
    sub = parser.add_subparsers(dest="command", required=True)

    sub.add_parser("health")
    sub.add_parser("list")

    search = sub.add_parser("search")
    search.add_argument("--query")
    search.add_argument("--dcc-type")
    search.add_argument("--limit", type=int)

    describe = sub.add_parser("describe")
    describe.add_argument("tool_slug")

    call = sub.add_parser("call")
    call.add_argument("tool_slug")
    call.add_argument("--json", default="{}")
    call.add_argument("--meta-json")
    return parser


def main() -> int:
    """CLI entry point."""
    args = build_parser().parse_args()
    payload = run_command(args.command, args)
    print(_json_dumps(payload, pretty=args.pretty))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


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