Skip to content

ucode configure skills: MCP-connection mode (add/remove/replace) + status#236

Draft
xsh310 wants to merge 3 commits into
databricks:mainfrom
xsh310:skills-mcp-mode
Draft

ucode configure skills: MCP-connection mode (add/remove/replace) + status#236
xsh310 wants to merge 3 commits into
databricks:mainfrom
xsh310:skills-mcp-mode

Conversation

@xsh310

@xsh310 xsh310 commented Jul 23, 2026

Copy link
Copy Markdown

Summary

Adds 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 (aligned to the backend route in databricks-eng/universe#2256555). Also adds a top-level Skills section to ucode status.

Self-contained PR off main (a small _setup_mcp_clients / build_mcp_http_entry refactor 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).
  • Single skills connection (kind:"skills", fixed name databricks-skill-registry) with skill_locations as 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/--replace are mutually exclusive and require --mcp; a soft warning fires above 10 schemas.
  • Bare 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 .skill location) without --mcp raises a clear "download mode is not available yet; use --mcp" error rather than silently accepting a no-op flag.
  • alwaysLoad is set on the skills entry for the Claude client only (rides the existing per-client apply); other clients get the URL only.
  • Behavior-preserving prep folded in: _setup_mcp_clients extracted from configure_mcp_command (shared by the mcp and skills commands), and an always_load keyword added to build_mcp_http_entry (default off).
  • ucode status gains 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_url for zero / one / many locations (order preserved).
  • _resolve_skills_mcp_servers single-entry rebuild, URL derived from skill_locations, mcp-service entries preserved, alwaysLoad set.
  • resolve_skill_location_set add / remove / replace / empty (order-stable, deduped).
  • configure_skills_mcp_command flows (add-to-empty, add-second, remove, replace, remove-last → bare route with the entry retained).
  • CLI two-mode dispatch, flag mutual-exclusion, comma --location parsing, 3-part .skill rejected under --mcp, download-only flags without --mcp rejected, malformed → error, missing --location → usage error; status Skills-section renderings.

Verified locally with uv run ruff check . (clean) and the full pytest suite: 930 passed, 39 skipped, 1 failed — the single failure is a pre-existing, environment-only test_e2e_user_agent.py case (the local claude binary is Claude Code, not the standalone Anthropic CLI) that also fails on main.

This pull request and its description were written by Isaac.

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
xsh310 added 2 commits July 23, 2026 16:55
`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
Comment thread src/ucode/cli.py
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add the 3-part handling in the later download mode PR instead

Comment thread src/ucode/cli.py
path: Annotated[
str | None, typer.Option("--path", help="(download) Project root; default user scope (~/).")
] = None,
yes: Annotated[

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not add this in later download mode PR

Comment thread src/ucode/cli.py
],
mcp: Annotated[
bool,
typer.Option("--mcp", help="Mutate the skills MCP connection instead of downloading."),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make this required and change this to optional when we add support for download?

Comment thread src/ucode/cli.py
Comment on lines +1353 to +1359
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,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we note that remove and replace is currently only supported for mcp mode?

Comment thread src/ucode/cli.py
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/ucode/cli.py
"Skill MCP Locations",
", ".join(locations) if locations else "none — utility tools only",
)
displays = [

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why this is variable called displays? Is there a more intuitive variable name for this?

Comment thread src/ucode/cli.py
console.print()

print_heading("Skills")
skills_entry = next((s for s in mcp_servers if s.get("kind") == SKILLS_MCP_KIND), None)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this skill_mcp_entry

Comment thread src/ucode/databricks.py


def build_skills_mcp_url(workspace: str, locations: list[str]) -> str:
"""Skills route with repeated ``?schema=`` scopes (universe PR #2256555).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tighten this cmd to only provide 2 examples, 1 for empty location, 1 for multiple location. Don't mention universe PR number

Comment thread src/ucode/mcp.py
}
SKILLS_MCP_KIND = "skills"
SKILLS_MCP_SERVER_NAME = "databricks-skill-registry"
SKILLS_MCP_SCHEMA_SOFT_CAP = 10

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine that we drop soft cap for now and have the backend return the errors directly

Comment thread src/ucode/mcp.py
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tighten this comment

Comment thread src/ucode/mcp.py


def _skills_entries(servers: list[dict]) -> list[dict]:
"""Skills connections carry their own lifecycle (``configure skills``); the

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this docstring, the function name self explantory

Comment thread src/ucode/mcp.py
return 0


def _apply_skills_connection(

@xsh310 xsh310 Jul 23, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: does it make sense to call this _update_skills_mcp instead?

Comment thread src/ucode/mcp.py
return 0


def configure_skills_command(locations: list[str]) -> int:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant