Skip to content
Closed
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
65 changes: 43 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,39 +189,57 @@ on — and removes the ones you select from those tools. It needs no Databricks

### Skills (optional)

Configure Unity Catalog Skills for your coding tools with `ucode configure skills`:
Use `ucode skill add` to download Unity Catalog Skills or expose them through the skills MCP:

```bash
# Utility tools only: register the schema-less skills MCP connection, no download.
ucode configure skills
# Browse finalized skills across the metastore and choose which ones to download.
ucode skill add

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

# Download a named subset of the schema's skills instead of all of them.
ucode configure skills --location main.default --skill my-skill
# Download named skills by bare name or fully-qualified name.
ucode skill add --location main.default --skills my-skill,other-skill
ucode skill add --skills main.default.my-skill,main.default.other-skill

# MCP mode: expose the schema's skills as MCP tools instead of downloading.
ucode configure skills --location main.default,ml.prod --mcp
# Add schemas to every configured agent's MCP scope, keeping existing schemas.
ucode skill add --location main.default,ml.prod --mcp

# Add a schema only to selected agents, setting them up first if needed.
ucode skill add --location main.default --mcp --agents claude,codex
```

- **Bare command** (no `--location`) registers the schema-less skills MCP connection — the
cross-schema utility tools only — and downloads nothing. `--mcp` with no `--location` does the
same.
- **Download mode** (with `--location`, no `--mcp`) 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, leaving any prior `--mcp` scope untouched. `--skill <name>[,<name>…]` narrows the
download to the named skills (by leaf name) from the schema instead of all of them; requested
names not found in the schema warn and are skipped. `--skill` requires a single `--location`, is
download-only, and is rejected with `--mcp`.
- **MCP mode** (`--location … --mcp`) sets the connection's location set to exactly `<list>`
(override-only) and rebuilds its `?schema=` URL; no files are downloaded and `--path` is rejected.
pre-existing skill dir prompts before it's overwritten. `--skills` narrows the download to named
securables. With neither `--location` nor `--skills`, a searchable picker lists the finalized
skills visible in the metastore. Bare names require one `--location`; fully-qualified names derive
it. Download mode always writes both directory families, so `--agents` is accepted only with
`--mcp`.
- **MCP mode** (`--location … --mcp`) unions schemas into the selected agents' existing scopes.
Without `--agents`, it updates every configured agent. No files are downloaded.

Each run prints the registered server, its URL, the configured agents, and its tools, and reminds
you to run `ucode <agent>` (existing agent sessions need a restart before the MCP tools load).

#### Remove skill MCP scopes

`ucode skill remove --mcp` interactively lists developer-configured schemas and the agents using
them. Selected schemas are removed from every agent, or only from `--agents` when supplied. The
schema-less utility connection remains registered after its last schema is removed. Downloaded
skill removal is not yet supported.

```bash
ucode skill remove --mcp
ucode skill remove --mcp --agents codex
```

`ucode configure skills` is deprecated but remains available for existing scripts. Its MCP mode
keeps its original replacement semantics, and its bare form remains the way to register only the
schema-less utility connection during the transition.


### Managed config for a workspace (admins)

Author the coding config your developers pick up automatically, instead of asking each of them to
Expand Down Expand Up @@ -352,10 +370,13 @@ The output looks like:
| `ucode mcp add --agents claude --services system.ai.slack` | Set up the agent(s) if needed and register the server for them |
| `ucode mcp remove` | Interactively unregister configured MCP servers from your coding tools |
| `ucode mcp remove --agents codex` | Unregister selected servers from specific agents only |
| `ucode configure skills` | Register the skills MCP connection (utility tools only); no skills download |
| `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 --skill my-skill` | Download only the named skill(s) from a schema (comma-separated for several) |
| `ucode configure skills --location main.default --mcp` | Expose a schema's skills as MCP tools (override-only) instead of downloading |
| `ucode skill add --location main.default --mcp` | Add schemas to the skills MCP scope, keeping any already configured (additive; never replaces) |
| `ucode skill add --location main.default --mcp --agents claude` | Set up selected agents if needed and add schemas only to their MCP scopes |
| `ucode skill add` | Interactively choose finalized metastore skills to download |
| `ucode skill add --location main.default` | Download a schema's skills to disk without removing existing downloads |
| `ucode skill add --skills main.default.my-skill` | Download a named subset of skills (bare names need `--location`; fully-qualified names stand alone) |
| `ucode skill remove --mcp [--agents codex]` | Interactively remove developer schemas from all or selected agent MCP scopes |
| `ucode configure skills ...` | Deprecated compatibility command; existing behavior is preserved |
| `ucode setup` | Author the managed config's agents and models (workspace admins only) |
| `ucode setup mcps` | Add or change the managed config's MCP servers |
| `ucode setup skills [--location a.b,c.d]` | Add or change the managed config's skills |
Expand Down
212 changes: 198 additions & 14 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,20 @@
MCP_CLIENTS,
SKILLS_MCP_KIND,
add_mcp_command,
add_skills_command,
apply_managed_mcp_servers,
apply_managed_skills,
configure_mcp_command,
configure_skills_mcp_command,
purge_cross_workspace_mcp_residue,
remove_mcp_command,
remove_skills_command,
revert_mcp_configs,
skill_locations_for_client,
)
from ucode.skills_download import (
configure_skills_download_command,
configure_skills_download_interactive_command,
download_managed_skills_on_launch,
)
from ucode.smart_routing import v2 as smart_routing_v2
Expand Down Expand Up @@ -1062,17 +1066,31 @@ def status() -> int:
if not skill_mcp_entry:
print_kv("Skills", "not configured")
else:
locations = skill_mcp_entry.get("skill_locations") or []
print_kv(
"Skill MCP Locations",
", ".join(locations) if locations else "none — utility tools only",
)
configured_agents = [
str(MCP_CLIENTS[client]["display"])
for client in (skill_mcp_entry.get("clients") or [])
if client in MCP_CLIENTS
configured_clients = [
client for client in (skill_mcp_entry.get("clients") or []) if client in MCP_CLIENTS
]
print_kv("Configured", ", ".join(configured_agents) if configured_agents else "none")
scopes = {
client: skill_locations_for_client(skill_mcp_entry, client)
for client in configured_clients
}
if len({tuple(locations) for locations in scopes.values()}) <= 1:
locations = next(
iter(scopes.values()), list(skill_mcp_entry.get("skill_locations") or [])
)
print_kv(
"Skill MCP Locations",
", ".join(locations) if locations else "none — utility tools only",
)
configured_agents = [
str(MCP_CLIENTS[client]["display"]) for client in configured_clients
]
print_kv("Configured", ", ".join(configured_agents) if configured_agents else "none")
else:
for client, locations in scopes.items():
print_kv(
f"{MCP_CLIENTS[client]['display']} skill MCP locations",
", ".join(locations) if locations else "none — utility tools only",
)

print_heading("Tracing")
tracing = state.get("tracing") or {}
Expand All @@ -1098,9 +1116,7 @@ def status() -> int:
print_note(
"Use `ucode configure mcp` to add Databricks MCP servers to configured coding tools."
)
print_note(
"Use `ucode configure skills` to set up Unity Catalog Skills for configured coding tools."
)
print_note("Use `ucode skill add` and `ucode skill remove --mcp` to manage UC Skills.")
print_note("Use `ucode configure tracing` to log coding sessions to an MLflow experiment.")
print_note("Use `ucode revert` to clear managed configs and restore prior files.")
return 0
Expand Down Expand Up @@ -1159,6 +1175,8 @@ def revert() -> int:
app.add_typer(configure_app, name="configure", help="Configure workspace and tool settings.")
mcp_app = typer.Typer(add_completion=False, no_args_is_help=True)
app.add_typer(mcp_app, name="mcp", help="MCP servers exposed by ucode.")
skill_app = typer.Typer(add_completion=False, no_args_is_help=True)
app.add_typer(skill_app, name="skill", help="Databricks Skills for your coding tools.")
setup_app = typer.Typer(add_completion=False, no_args_is_help=False)
app.add_typer(
setup_app,
Expand Down Expand Up @@ -1312,6 +1330,168 @@ def mcp_web_search_cmd() -> None:
serve()


@skill_app.command("add")
def skills_add(
location: Annotated[
str | None,
typer.Option(
"--location", help="Comma-separated `<catalog>.<schema>` skill scopes to add."
),
] = None,
mcp: Annotated[
bool,
typer.Option(
"--mcp",
help="Add the schemas to the skills MCP connection's scope instead of downloading.",
),
] = False,
path: Annotated[
str | None,
typer.Option(
"--path",
help="(download) Existing absolute dir to download into; defaults to your home dir.",
),
] = None,
skills: Annotated[
str | None,
typer.Option(
"--skills",
help="(download) Download only this comma-separated subset of skills instead of "
"every skill in the schema. Bare securable names (e.g. `my-skill`) need a single "
"--location; fully-qualified `<catalog>.<schema>.<name>` names work on their own. "
"Not valid with --mcp.",
),
] = None,
agents: Annotated[
str | None,
typer.Option(
"--agents",
help="(--mcp only) Comma-separated coding agents whose skills MCP scope should "
"be updated. Any that aren't configured yet are set up first.",
),
] = None,
) -> None:
"""Add Databricks Skills to your coding tools, keeping any already configured.

With ``--mcp``, adds the given schemas to the skills MCP connection's scope.
Otherwise downloads each schema's skills to disk (under ``--path``, or your home
dir), keeping already-downloaded skills. With no selection flags, opens a searchable
metastore picker. ``--skills`` narrows a download to a subset of one schema's skills,
by bare name (with ``--location``) or fully-qualified
``<catalog>.<schema>.<name>``.
"""
try:
locations = _parse_skill_locations(location)
requested_skills = (
None if skills is None else {s.strip() for s in skills.split(",") if s.strip()}
)
requested_agents = (
None
if agents is None
else ({agent.strip().lower() for agent in agents.split(",") if agent.strip()} or None)
)
if mcp and path is not None:
raise RuntimeError("--path is not supported when using --mcp")
if mcp and requested_skills is not None:
raise RuntimeError("--skills is not supported when using --mcp")
if not mcp and agents is not None:
raise RuntimeError("--agents is only supported when using --mcp")
if requested_skills is not None and not locations:
schemas = {".".join(s.split(".")[:2]) for s in requested_skills if s.count(".") >= 2}
bare = sorted(s for s in requested_skills if s.count(".") < 2)
if bare:
raise RuntimeError(
"--skills short names need --location (or pass full names like "
f"`<catalog>.<schema>.<name>`): {', '.join(bare)}"
)
if len(schemas) != 1:
raise RuntimeError(
"--skills without --location must all share one `<catalog>.<schema>` "
f"(got: {', '.join(sorted(schemas)) or 'none'}); pass --location instead."
)
locations = list(schemas)
if not locations:
if mcp:
raise RuntimeError("--location is required when using --mcp.")
configure_skills_download_interactive_command(path=path)
return
if requested_skills is not None and len(locations) != 1:
raise RuntimeError(
f"--skills requires a single --location (got: {', '.join(locations)})."
)
mismatched_skills = sorted(
skill
for skill in requested_skills or set()
if skill.count(".") >= 2 and ".".join(skill.split(".")[:2]) != locations[0]
)
if mismatched_skills:
raise RuntimeError(
f"--skills entries must match --location `{locations[0]}` "
f"(got: {', '.join(mismatched_skills)})."
)
selected_skills = (
None if requested_skills is None else {s.split(".")[-1] for s in requested_skills}
)
if mcp:
scope = (
_configure_agents_for_mcp(sorted(requested_agents)) if requested_agents else None
)
if scope is None:
add_skills_command(locations)
else:
add_skills_command(locations, agents=scope)
else:
configure_skills_download_command(locations, path=path, skills=selected_skills)
except (RuntimeError, ValueError) as exc:
print_err(str(exc))
raise typer.Exit(1) from None
except KeyboardInterrupt:
print_err("Interrupted.")
raise typer.Exit(130) from None


@skill_app.command("remove")
def skills_remove(
mcp: Annotated[
bool,
typer.Option(
"--mcp",
help="Remove schemas from the skills MCP connection instead of downloaded files.",
),
] = False,
agents: Annotated[
str | None,
typer.Option(
"--agents",
help="Comma-separated coding agents to remove selected schemas from.",
),
] = None,
) -> None:
"""Remove Databricks Skill schemas from coding tools.

MCP removal is interactive and can be scoped to specific configured agents.
Removing downloaded skills is not supported yet.
"""
try:
if not mcp:
raise RuntimeError(
"Removing downloaded skills is not supported yet. Pass --mcp to remove "
"schemas from the skills MCP connection."
)
requested_agents = (
None
if agents is None
else ({agent.strip().lower() for agent in agents.split(",") if agent.strip()} or None)
)
remove_skills_command(agents=requested_agents)
except RuntimeError as exc:
print_err(str(exc))
raise typer.Exit(1) from None
except KeyboardInterrupt:
print_err("Interrupted.")
raise typer.Exit(130) from None


@app.command("mcp-proxy", hidden=True)
def mcp_proxy_cmd(
url: Annotated[
Expand Down Expand Up @@ -2913,7 +3093,7 @@ def configure_mcp(
raise typer.Exit(130) from None


@configure_app.command("skills")
@configure_app.command("skills", deprecated=True)
def configure_skills(
location: Annotated[
str | None,
Expand Down Expand Up @@ -2952,6 +3132,10 @@ def configure_skills(
download to a named subset of a single schema's skills (requires exactly one
``--location``).
"""
print_warning(
"`ucode configure skills` is deprecated. Use `ucode skill add` to download "
"skills or add MCP scopes, and `ucode skill remove --mcp` to remove MCP scopes."
)
try:
locations = _parse_skill_locations(location)
# `--skill` absent -> None (whole schema); present (even empty) -> the
Expand Down
Loading
Loading