ucode configure skills: MCP-connection mode (add/remove/replace) + status#236
ucode configure skills: MCP-connection mode (add/remove/replace) + status#236xsh310 wants to merge 3 commits into
Conversation
Introduces `ucode configure skills` for managing a single Unity Catalog Skills MCP connection (`databricks-skill-registry`) whose scope is a set of `catalog.schema` locations, rendered as a multi-schema `?schema=` URL (aligns with the backend route in universe #2256555). - `build_skills_mcp_url(workspace, locations)`: repeated `?schema=`; empty list -> bare `/ai-gateway/skills/` (utility tools only). - Single `kind:"skills"` entry with `skill_locations` as the source of truth; the URL is derived and never parsed back. Every run rebuilds exactly one skills entry, preserving non-skills entries. - `--location a.b,c.d --mcp [--remove|--replace]` adds/removes/replaces the location set; `--remove`/`--replace` are mutually exclusive and require `--mcp`; a soft warning fires above 10 schemas. The bare form (no `--mcp`) registers the connection for the given schemas; download-only affordances (`--path`, `--yes`, a 3-part `.skill` location) without `--mcp` raise a clear "not available yet" error. - `alwaysLoad` is set on the skills entry for the Claude client only. - `ucode status` gains a top-level Skills section (Connection / Mode / Locations / Configured); the skills entry is excluded from each agent's per-client MCP-servers line. Also extracts `_setup_mcp_clients` from `configure_mcp_command` (shared by the mcp and skills commands) and adds an `always_load` keyword to `build_mcp_http_entry` (default off) as behavior-preserving prep. Co-authored-by: Isaac
`configure mcp --location` (and the interactive picker) rebuilt the MCP server list from only the mcp-services they manage, so `apply_mcp_server_changes` treated the `kind:"skills"` entry as a removal and unregistered it from every agent. Both paths now carry an existing skills connection through untouched (the picker also hides it, since it is managed by `configure skills`). Co-authored-by: Isaac
Drop the Connection line (fixed name, no signal) and the Mode line (inferred; deferred until download mode lands), and rename the locations line to `Skill MCP Locations` to distinguish it from download locations a later PR may track separately. Co-authored-by: Isaac
| def _parse_skill_locations(location: str, *, allow_skill: bool) -> list[SkillLocation]: | ||
| """Parse a comma-separated `--location` into `SkillLocation`s. | ||
|
|
||
| Each entry is a 2-part `<catalog>.<schema>` or 3-part |
There was a problem hiding this comment.
Let's add the 3-part handling in the later download mode PR instead
| path: Annotated[ | ||
| str | None, typer.Option("--path", help="(download) Project root; default user scope (~/).") | ||
| ] = None, | ||
| yes: Annotated[ |
There was a problem hiding this comment.
Let's not add this in later download mode PR
| ], | ||
| mcp: Annotated[ | ||
| bool, | ||
| typer.Option("--mcp", help="Mutate the skills MCP connection instead of downloading."), |
There was a problem hiding this comment.
Can you make this required and change this to optional when we add support for download?
| remove: Annotated[ | ||
| bool, typer.Option("--remove", help="(--mcp) Remove the listed schemas from the set.") | ||
| ] = False, | ||
| replace: Annotated[ | ||
| bool, | ||
| typer.Option("--replace", help="(--mcp) Replace the set with exactly the listed schemas."), | ||
| ] = False, |
There was a problem hiding this comment.
Should we note that remove and replace is currently only supported for mcp mode?
| locs = _parse_skill_locations(location, allow_skill=False) | ||
| configure_skills_mcp_command([loc.schema_ref for loc in locs], mode=mode) | ||
| else: | ||
| # Download mode lands in a later PR; until then, reject every |
There was a problem hiding this comment.
Should we just return not implemented error here for now?
Also is configure_skills_command([loc.schema_ref for loc in locs]) correct? I thought we only provide static tools when in download mode
| "Skill MCP Locations", | ||
| ", ".join(locations) if locations else "none — utility tools only", | ||
| ) | ||
| displays = [ |
There was a problem hiding this comment.
nit: why this is variable called displays? Is there a more intuitive variable name for this?
| console.print() | ||
|
|
||
| print_heading("Skills") | ||
| skills_entry = next((s for s in mcp_servers if s.get("kind") == SKILLS_MCP_KIND), None) |
There was a problem hiding this comment.
Can we call this skill_mcp_entry
|
|
||
|
|
||
| def build_skills_mcp_url(workspace: str, locations: list[str]) -> str: | ||
| """Skills route with repeated ``?schema=`` scopes (universe PR #2256555). |
There was a problem hiding this comment.
Tighten this cmd to only provide 2 examples, 1 for empty location, 1 for multiple location. Don't mention universe PR number
| } | ||
| SKILLS_MCP_KIND = "skills" | ||
| SKILLS_MCP_SERVER_NAME = "databricks-skill-registry" | ||
| SKILLS_MCP_SCHEMA_SOFT_CAP = 10 |
There was a problem hiding this comment.
I think it's fine that we drop soft cap for now and have the backend return the errors directly
| if not isinstance(url, str) or not url: | ||
| continue | ||
| entry = build_mcp_http_entry(url) | ||
| # alwaysLoad is Claude-only and rides the per-entry apply; other clients |
|
|
||
|
|
||
| def _skills_entries(servers: list[dict]) -> list[dict]: | ||
| """Skills connections carry their own lifecycle (``configure skills``); the |
There was a problem hiding this comment.
remove this docstring, the function name self explantory
| return 0 | ||
|
|
||
|
|
||
| def _apply_skills_connection( |
There was a problem hiding this comment.
nit: does it make sense to call this _update_skills_mcp instead?
| return 0 | ||
|
|
||
|
|
||
| def configure_skills_command(locations: list[str]) -> int: |
Summary
Adds
ucode configure skillsfor managing a single Unity Catalog Skills MCP connection (databricks-skill-registry) whose scope is a set ofcatalog.schemalocations, rendered as a multi-schema?schema=URL (aligned to the backend route in databricks-eng/universe#2256555). Also adds a top-level Skills section toucode status.Self-contained PR off
main(a small_setup_mcp_clients/build_mcp_http_entryrefactor that was originally split into a separate PR has been folded in here).What's in this PR
build_skills_mcp_url(workspace, locations)— multi-schema?schema=a&schema=b; empty list → bare/ai-gateway/skills/(utility tools only).kind:"skills", fixed namedatabricks-skill-registry) withskill_locationsas the source of truth; the URL is derived, never parsed back. Every run rebuilds exactly one skills entry (single-connection invariant); non-skills entries are preserved.ucode configure skills --location a.b,c.d --mcp [--remove|--replace]— add / remove / replace the location set.--remove/--replaceare mutually exclusive and require--mcp; a soft warning fires above 10 schemas.ucode configure skills --location a.b(no--mcp) registers the connection for those schemas. Download mode is a planned follow-up, so supplying a download-only affordance (--path,--yes, or a 3-part.skilllocation) without--mcpraises a clear "download mode is not available yet; use --mcp" error rather than silently accepting a no-op flag.alwaysLoadis set on the skills entry for the Claude client only (rides the existing per-client apply); other clients get the URL only._setup_mcp_clientsextracted fromconfigure_mcp_command(shared by the mcp and skills commands), and analways_loadkeyword added tobuild_mcp_http_entry(default off).ucode statusgains a Skills section (Connection / Mode / Locations / Configured); the skills entry is excluded from each agent's per-client MCP-servers line to avoid double-listing.How do you know it works?
New unit tests across
tests/test_databricks.py,tests/test_mcp.py,tests/test_cli.py:build_skills_mcp_urlfor zero / one / many locations (order preserved)._resolve_skills_mcp_serverssingle-entry rebuild, URL derived fromskill_locations, mcp-service entries preserved,alwaysLoadset.resolve_skill_location_setadd / remove / replace / empty (order-stable, deduped).configure_skills_mcp_commandflows (add-to-empty, add-second, remove, replace, remove-last → bare route with the entry retained).--locationparsing, 3-part.skillrejected under--mcp, download-only flags without--mcprejected, malformed → error, missing--location→ usage error;statusSkills-section renderings.Verified locally with
uv run ruff check .(clean) and the fullpytestsuite: 930 passed, 39 skipped, 1 failed — the single failure is a pre-existing, environment-onlytest_e2e_user_agent.pycase (the localclaudebinary is Claude Code, not the standalone Anthropic CLI) that also fails onmain.This pull request and its description were written by Isaac.