Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Tests live in `tests/`.

- Use Python 3.12+.
- Keep changes scoped to the requested behavior.
- Follow the existing module boundaries: CLI orchestration in `cli.py`, agent-specific behavior in `agents/<name>.py`, shared agent dispatch in `agents/__init__.py`, Databricks calls in `databricks.py`, and presentation helpers in `ui.py`.
- Follow the existing module boundaries: CLI orchestration in `cli.py`, agent-specific behavior in `agents/<name>.py`, shared agent dispatch in `agents/__init__.py`, Databricks calls in `databricks.py`, skill download (UC fetch client + on-disk writer + download orchestration) in `skills_download.py`, MCP-connection state glue in `mcp.py`, and presentation helpers in `ui.py`. Skill download persists no disk state — it writes files to `--path` (or the home dir) and registers only the schema-less skills MCP connection.
- Prefer existing helpers for config file writes, state persistence, UI messages, and Databricks authentication.
- Add or update focused tests for behavior changes.
- Do not modify generated or lock files unless the dependency graph intentionally changes.
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,28 @@ ucode configure --agents claude --mcp system.ai.slack
`--mcp` also works without `--agents` for MCP-only clients (it configures just the workspace,
then registers the servers); pass a comma-separated list to register several at once.

### Skills (optional)

Configure Unity Catalog Skills for your coding tools with `ucode configure skills`. It has two
mutually-exclusive modes, both scoped by `--location <catalog>.<schema>` (comma-separated for
multiple schemas):

```bash
# Download mode (default): fetch every skill in the schema to disk.
ucode configure skills --location main.default --path /abs/project/dir

# MCP mode: expose the schema's skills as MCP tools instead of downloading.
ucode configure skills --location main.default,ml.prod --mcp
```

- **Download mode** writes each skill flat as `<leaf>/SKILL.md` (plus its bundled files) into both
`.claude/skills/` and `.agents/skills/`. `--path` (an existing absolute directory) is optional;
when omitted, skills are written under your home directory. Any pre-existing skill dir prompts
before it's overwritten. It then registers a schema-less skills MCP connection (utility tools
only), leaving any prior `--mcp` scope untouched.
- **MCP mode** sets the connection's location set to exactly `<list>` (override-only) and rebuilds
its `?schema=` URL; no files are downloaded and `--path` is rejected.

---

## Other Commands
Expand All @@ -118,6 +140,8 @@ then registers the servers); pass a comma-separated list to register several at
| `ucode configure --profiles DEFAULT --use-pat` | Authenticate with the profile's personal access token — no browser login |
| `ucode configure --skip-validate` | Write configs without sending a test message through each agent |
| `ucode configure --agents claude --mcp system.ai.slack` | Configure an agent and register its Databricks MCP server(s) in one command |
| `ucode configure skills --location main.default [--path <dir>]` | Download a schema's skills to disk (under `<dir>`, or your home dir) and register a schema-less skills MCP connection |
| `ucode configure skills --location main.default --mcp` | Expose a schema's skills as MCP tools (override-only) instead of downloading |

## Managed Local Files

Expand Down
27 changes: 19 additions & 8 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
purge_cross_workspace_mcp_residue,
revert_mcp_configs,
)
from ucode.skills_download import configure_skills_download_command
from ucode.state import (
STATE_PATH,
clear_state,
Expand Down Expand Up @@ -1403,21 +1404,31 @@ def configure_skills(
],
mcp: Annotated[
bool,
typer.Option("--mcp", help="Mutate the skills MCP connection instead of downloading."),
] = False,
path: Annotated[
str | None,
typer.Option(
"--mcp", help="Manage the skills MCP connection (required until download mode lands)."
"--path",
help="(download) Existing absolute dir to download into; defaults to your home dir.",
),
] = False,
] = None,
) -> None:
"""Configure Databricks Skills for your coding tools.

``--location`` sets the skills MCP connection's scope to exactly the listed
schemas, replacing any previous set.
By default, downloads every skill in each ``--location`` schema to disk
(under ``--path``, or your home dir when omitted) and registers a schema-less
MCP connection. With ``--mcp``, instead sets the skills MCP connection's scope
to exactly the listed schemas.
"""
try:
if not mcp:
raise RuntimeError("Download mode is not available yet; pass --mcp for now.")
configure_skills_mcp_command(_parse_skill_locations(location))
except RuntimeError as exc:
if mcp:
if path is not None:
raise RuntimeError("--path is not valid with --mcp.")
configure_skills_mcp_command(_parse_skill_locations(location))
else:
configure_skills_download_command(_parse_skill_locations(location), path=path)
except (RuntimeError, ValueError) as exc:
print_err(str(exc))
raise typer.Exit(1) from None
except KeyboardInterrupt:
Expand Down
29 changes: 29 additions & 0 deletions src/ucode/databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,35 @@ def _http_post_json(
return None, f"network error: {exc.reason}"


def _http_get_bytes(url: str, token: str, *, timeout: int = 10) -> tuple[bytes | None, str | None]:
"""GET raw bytes. Returns (body, None) on success, (None, reason) on failure.

Like `_http_get_json` but leaves the body undecoded, since skill bundles can
contain binary files.
"""
request = urllib_request.Request(url, headers={"Authorization": f"Bearer {token}"})
try:
with urllib_request.urlopen(request, timeout=timeout) as response:
body = response.read()
_debug(f"GET {url}", f"HTTP 200, {len(body)} bytes")
return body, None
except urllib_error.HTTPError as exc:
detail = ""
try:
detail = exc.read().decode("utf-8", errors="replace") if exc.fp else ""
except Exception:
detail = ""
_debug(f"GET {url}", f"HTTP {exc.code} {exc.reason}")
reason = f"HTTP {exc.code} {exc.reason}"
excerpt = detail.strip()[:200]
if excerpt:
reason = f"{reason}: {excerpt}"
return None, reason
except urllib_error.URLError as exc:
_debug(f"GET {url}", f"URLError: {exc.reason}")
return None, f"network error: {exc.reason}"


def get_current_user_name(workspace: str, token: str) -> str | None:
"""Return the current user's login (email) via SCIM `Me`, or None on failure.

Expand Down
21 changes: 18 additions & 3 deletions src/ucode/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,7 @@ def _resolve_location_mcp_servers(
return [*working_servers, *_skills_entries(original_servers)]


def _setup_mcp_clients(state: dict, section: str) -> tuple[str, str | None, list[str]]:
def setup_mcp_clients(state: dict, section: str) -> tuple[str, str | None, list[str]]:
"""Validate the workspace, resolve configured MCP clients, and prepare auth.

Returns ``(workspace, profile, clients)`` and prints the section header, the
Expand Down Expand Up @@ -1253,7 +1253,7 @@ def configure_mcp_command(location: str | None = None, services: set[str] | None
)
location = next(iter(schemas))
state = load_state()
workspace, profile, clients = _setup_mcp_clients(state, "MCP Servers")
workspace, profile, clients = setup_mcp_clients(state, "MCP Servers")

original_mcp_servers_for_location: list[dict] = list(state.get("mcp_servers") or [])
if location is not None:
Expand Down Expand Up @@ -1424,6 +1424,21 @@ def configure_skills_mcp_command(locations: list[str]) -> int:
"""Set the skills MCP connection's ``skill_locations`` to exactly ``locations``,
replacing any previous set."""
state = load_state()
workspace, _profile, clients = _setup_mcp_clients(state, "Skills MCP")
workspace, _profile, clients = setup_mcp_clients(state, "Skills MCP")
_update_skills_mcp(state, workspace, clients, locations)
return 0


def _skill_mcp_locations(state: dict) -> list[str]:
"""The skills MCP connection's ``skill_locations``, or ``[]`` if none exists."""
entry = next(iter(_skills_entries(list(state.get("mcp_servers") or []))), None)
return list((entry or {}).get("skill_locations") or [])


def register_schemaless_skills_connection(state: dict, workspace: str, clients: list[str]) -> None:
"""Register/keep the skills MCP connection without changing its schema set.

Download mode calls this after writing files: it preserves any prior
``--mcp`` ``skill_locations`` and otherwise registers the bare schema-less
route (utility tools only)."""
_update_skills_mcp(state, workspace, clients, _skill_mcp_locations(state))
Loading
Loading