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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,33 @@ ucode configure skills --location main.default,ml.prod --mcp
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).

#### Add skill scopes without replacing existing ones

`ucode skill add` registers skills additively, keeping anything already configured. With `--mcp` it
adds the schemas to every configured agent's scope, or only to `--agents` when supplied. Agents that
are not configured yet are set up first. Without `--mcp`, it downloads skills to disk; download mode
always writes both directory families and does not accept `--agents`. `--skills` narrows a download
to a subset of one schema's skills. With no selection flags, a searchable picker lists finalized
skills visible in the metastore.

```bash
# Browse the metastore and choose skills to download.
ucode skill add

# Add schemas to the skills MCP scope, keeping any already configured.
ucode skill add --location main.default,ml.prod --mcp

# Add a schema only to selected agents.
ucode skill add --location main.default --mcp --agents claude,codex

# Download a schema's skills to disk, keeping existing downloads.
ucode skill add --location main.default

# Download a named subset, by bare name (with --location) 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
```

### 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 @@ -356,6 +383,11 @@ The output looks like:
| `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` | Interactively choose finalized metastore skills to download |
| `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 --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 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
159 changes: 149 additions & 10 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,19 @@
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,
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 +1065,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 Down Expand Up @@ -1159,6 +1176,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 +1331,126 @@ 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


@app.command("mcp-proxy", hidden=True)
def mcp_proxy_cmd(
url: Annotated[
Expand Down
Loading
Loading