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
72 changes: 46 additions & 26 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
remove_downloaded_skills_command,
)
from ucode.skills_list import list_configured_skills_command
from ucode.skills_state import records_for_scope
from ucode.smart_routing import v2 as smart_routing_v2
from ucode.smart_routing.claude_hooks import FIRST_PROMPT_SOCKET_ENV, ROUTE_FIRST_PROMPT_EVENT
from ucode.state import (
Expand Down Expand Up @@ -197,13 +198,29 @@ def _policy_summary_lines(managed: dict) -> list[str]:
return lines


def _configured_summary(names: list[str], *, limit: int = 5) -> str:
"""Format a resolved MCP/skill set for the Configuration panel as ``N (name, ..., name)``.

Empty renders "none configured": the admin set none, or a fetch failed and warned above. At most
``limit`` names are shown, with the rest collapsed to an ellipsis so a large set stays scannable.
"""
unique = sorted({name for name in names if name})
if not unique:
return "[dim]none configured[/dim]"
shown = unique[:limit]
if len(unique) > limit:
shown.append("...")
return f"{len(unique)} ({', '.join(shown)})"


def _print_managed_summary(
managed: dict,
state: dict,
tool: str | None,
*,
abridged: bool = False,
configured_tools: list[str] | None = None,
registered_mcps: list[str] | None = None,
) -> None:
"""Show which of the admin's settings are in force.

Expand Down Expand Up @@ -243,23 +260,16 @@ def _print_managed_summary(
model = managed_default_model(managed, tool)
if model:
lines.append(f"[bold]Model:[/bold] [magenta]{model}[/magenta]")
# Always listed, including when empty: "none configured" tells a developer their admin set none,
# which a missing row leaves ambiguous. Shown as the admin configured them — registering them
# locally is a separate change, hence "pending".
mcp_names = [
str(server.get("name"))
for server in (managed.get("mcp_servers") or [])
if isinstance(server, dict) and server.get("name")
# Count what ug actually registered/downloaded, not the admin's raw selector (a UC location is
# just a pointer with no count): the MCP servers reconcile registered this run, and the managed
# skills on disk. State can't stand in for the MCPs — #717 writes them to OS-managed files.
lines.append(f"[bold]MCPs:[/bold] {_configured_summary(registered_mcps or [])}")
skill_names = [
str(record.get("bundle_name"))
for record in records_for_scope("managed")
if record.get("bundle_name")
]
if mcp_names:
lines.append(f"[bold]MCPs:[/bold] {', '.join(mcp_names)} [dim](pending)[/dim]")
else:
lines.append("[bold]MCPs:[/bold] [dim]none configured[/dim]")
skill_names = [str(name) for name in ((managed.get("skills") or {}).get("names") or []) if name]
if skill_names:
lines.append(f"[bold]Skills:[/bold] {', '.join(skill_names)} [dim](pending)[/dim]")
else:
lines.append("[bold]Skills:[/bold] [dim]none configured[/dim]")
lines.append(f"[bold]Skills:[/bold] {_configured_summary(skill_names)}")
lines.extend(_policy_summary_lines(managed))
console.print(Panel("\n".join(lines), title="Configuration", style="green", expand=False))

Expand All @@ -285,10 +295,16 @@ def _print_managed_summary_abridged(managed: dict, state: dict, tool: str | None
)


def _summarize_managed_config(managed: dict, workspace: str, configured_tools: list[str]) -> None:
def _summarize_managed_config(
managed: dict, configured_tools: list[str], registered_mcps: list[str]
) -> None:
"""Show the resulting managed setup, listing only the agents that configured cleanly."""
_print_managed_summary(
managed, {"workspace": workspace}, tool=None, configured_tools=configured_tools
managed,
load_state(),
tool=None,
configured_tools=configured_tools,
registered_mcps=registered_mcps,
)
print_success("Configuration complete — launch with [bold cyan]ug[/bold cyan].")

Expand Down Expand Up @@ -877,10 +893,11 @@ def configure_workspace_command(
"None of the coding agents enabled by your workspace configuration "
"are available on this workspace."
)
registered_mcps: list[str] = []
if not is_dry_run():
_configure_managed_mcp_servers(managed)
registered_mcps = _configure_managed_mcp_servers(managed)
_configure_managed_skills(managed)
_summarize_managed_config(managed, state["workspace"], configured_tools)
_summarize_managed_config(managed, configured_tools, registered_mcps)
return 0

available_on_workspace: list[str] = []
Expand Down Expand Up @@ -2343,25 +2360,28 @@ def _print_budget_panel(recommendation: dict, tool: str, managed: dict | None =
console.print(panel)


def _configure_managed_mcp_servers(managed: dict | None) -> None:
def _configure_managed_mcp_servers(managed: dict | None) -> list[str]:
"""Register the managed config's MCP servers for every enabled MCP-client agent.

Runs during ``ug configure`` after the enabled agents are configured, so a workspace-published
server reaches each agent's `/mcp` list without the developer re-adding it. ``managed`` is None
when the (now-current) workspace has no managed config: the reconcile then unregisters any
servers a prior managed workspace registered, so switching workspaces resets the MCP registry.
Best-effort: a failure warns and leaves the rest of configure intact.
Best-effort: a failure warns and leaves the rest of configure intact. Returns the names of the
servers registered this run (the completion summary counts them; state alone can't, since #717
writes them to agents' OS-managed files rather than ``managed_mcp_servers``).
"""
managed = managed or {}
agents = {tool for tool in managed_enabled_tools(managed) if tool in MCP_CLIENTS}
try:
registered = reconcile_managed_mcp_servers(managed, agents)
except RuntimeError as exc:
print_warning(f"Could not register your workspace's MCP servers: {exc}")
return
if registered:
names = ", ".join(str(server["name"]) for server in registered)
print_note(f"Registered workspace MCP server(s): {names}")
return []
names = [str(server["name"]) for server in registered if server.get("name")]
if names:
print_note(f"Registered workspace MCP server(s): {', '.join(names)}")
return names


def _configure_managed_skills(managed: dict | None) -> None:
Expand Down
40 changes: 38 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2762,6 +2762,40 @@ def test_cursor_launch_uses_unity_gateway_branding():
assert "Unity Gateway with Cursor" in result.output


@pytest.mark.parametrize(
"names,expected",
[
(["b.mcp", "a.mcp"], "2 (a.mcp, b.mcp)"), # counted, deduped, sorted
(["a.mcp", "a.mcp"], "1 (a.mcp)"),
([], "[dim]none configured[/dim]"),
(["", None], "[dim]none configured[/dim]"),
([f"s{i}" for i in range(7)], "7 (s0, s1, s2, s3, s4, ...)"), # truncated past 5
],
)
def test_configured_summary(names, expected):
from ucode.cli import _configured_summary

assert _configured_summary(names) == expected


def test_print_managed_summary_counts_registered_mcps_and_skills():
# The `ug configure` completion panel counts the MCP servers reconcile registered this run and
# the managed skills ug wrote to disk, not the admin's raw selector (AIGTWY-4789).
from ucode.cli import _print_managed_summary, console

managed = {"enabled_agents": {"claude": {}, "codex": {}}}
state = {"workspace": "https://example.databricks.com"}
with patch("ucode.cli.records_for_scope", return_value=[{"bundle_name": "debug-ci"}]):
with console.capture() as capture:
_print_managed_summary(
managed, state, tool=None, registered_mcps=["jira-mcp", "github-mcp"]
)

output = re.sub(r"\s+", " ", capture.get())
assert "MCPs: 2 (github-mcp, jira-mcp)" in output
assert "Skills: 1 (debug-ci)" in output


class TestConfigureAgentFlag:
def test_no_flag_calls_configure_all(self):
with (
Expand Down Expand Up @@ -3283,13 +3317,15 @@ def test_managed_config_applies_all_enabled_and_skips_selection(self, monkeypatc
assert installed == ["claude", "codex"]
assert configured == ["claude", "codex"]

def test_managed_summary_separates_configured_and_failed_agents(self, capsys):
def test_managed_summary_separates_configured_and_failed_agents(self, capsys, monkeypatch):
# An enabled agent that failed to configure is listed under "Failed to configure",
# not as a configured coding agent.
import ucode.cli as cli_mod

managed = {"enabled_agents": {"claude": {}, "codex": {}}}
cli_mod._summarize_managed_config(managed, "https://w.com", ["claude"])
monkeypatch.setattr(cli_mod, "load_state", lambda: {"workspace": "https://w.com"})
monkeypatch.setattr(cli_mod, "records_for_scope", lambda scope: [])
cli_mod._summarize_managed_config(managed, ["claude"], [])

out = capsys.readouterr().out
# The rich panel wraps lines, so match on the labels and names rather than exact spacing.
Expand Down
Loading