Skip to content
Draft
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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,35 @@ summarized as a per-agent count. Skills connections are managed separately (via
`ug configure skills`) and aren't listed here. It reads local state plus each installed agent's
`mcp list`, so it needs no Databricks login.

#### Sign in to connection-backed servers

Some MCP services (e.g. `system.ai.github`) are backed by a Unity Catalog connection and only
vend their tools once you've completed a one-time per-user sign-in to the underlying SaaS. Use
`ug mcp login` to see which of your configured MCP services are signed in vs. still need a
sign-in, and to complete the sign-in:

```bash
# Show every configured connection-backed MCP service with its sign-in status,
# and pick which to sign in to.
ug mcp login

# Sign in to specific services non-interactively (full or short names).
ug mcp login --services system.ai.github,system.ai.slack

# Scope to specific agents' services.
ug mcp login --agents claude,codex
```

It uses the same configured-server set as `ug mcp list`, keeping only the connection-backed AI
Gateway MCP services, and shows each one's sign-in status (`signed in` / `needs sign-in`). Sign-in
opens your browser to complete the connection's login (via `databricks auth login`), then mints the
credential. The credential is **per-user and shared across every agent** — signing in once through
any agent (or here) unblocks that MCP service for Claude Code, Cursor, Codex, and the rest. It works
for any connection-backed MCP service, not just `system.ai.*`.

> Requires a Databricks CLI that supports `--resource` (databricks/cli#6621); `ug mcp login`
> reports a clear message if your CLI is too old.

### Skills (optional)

Configure Unity Catalog Skills for your coding tools with `ug configure skills`:
Expand Down Expand Up @@ -438,6 +467,8 @@ The output looks like:
| `ug mcp remove --agents codex` | Unregister selected servers from specific agents only |
| `ug mcp list` | List configured MCP servers and their live per-agent connection status |
| `ug mcp list --agents claude` | Show the connection-status report for specific agents only |
| `ug mcp login` | Show configured connection-backed MCP services' sign-in status and sign in to the ones you pick |
| `ug mcp login --services system.ai.github` | Sign in to specific connection-backed MCP service(s) non-interactively |
| `ug configure skills` | Register the skills MCP connection (utility tools only); no skills download |
| `ug 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 |
| `ug configure skills --skill main.default.my-skill` | Download named skills by fully-qualified name (comma-separated; may span schemas) |
Expand Down
44 changes: 44 additions & 0 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
revert_mcp_configs,
skill_locations_for_client,
)
from ucode.mcp_login import login_mcp_command
from ucode.skills_download import (
configure_location_skills_download_command,
configure_selected_skills_download_command,
Expand Down Expand Up @@ -1232,6 +1233,49 @@ def mcp_list(
raise typer.Exit(130) from None


@mcp_app.command("login")
def mcp_login(
services: Annotated[
str | None,
typer.Option(
"--services",
help="Sign in to this comma-separated subset of MCP services non-interactively. "
"Full names like `system.ai.github` or bare short names like `github` both work. "
"Omit --services to show the interactive picker with each service's sign-in status.",
),
] = None,
agents: Annotated[
str | None,
typer.Option(
"--agents",
help="Comma-separated coding agents to scope to (e.g. claude,codex). Without "
"--agents, considers the MCP services configured for every agent.",
),
] = None,
) -> None:
"""Sign in to the connection-backed MCP services your agents use.

Shows which configured MCP services are already signed in vs. need a
connection sign-in, and runs the sign-in for the ones you pick (or all named
with --services). Sign-in uses `databricks auth login --resource`, so it
works for any connection-backed MCP service (not just `system.ai.*`).
"""
selected = None if services is None else {s.strip() for s in services.split(",") if s.strip()}
requested_agents = (
None
if agents is None
else ({a.strip().lower() for a in agents.split(",") if a.strip()} or None)
)
try:
login_mcp_command(services=selected, 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


@mcp_app.command("web-search")
def mcp_web_search_cmd() -> None:
"""Run the web_search MCP server over stdio. Invoked as a subprocess by Claude Code."""
Expand Down
56 changes: 34 additions & 22 deletions src/ucode/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1977,6 +1977,37 @@ def _row_status(
)


def configured_mcp_servers_by_name(
state: dict, agents: set[str] | None = None
) -> dict[str, dict[str, Any]]:
"""Merge the developer- and workspace-managed MCP servers ug has configured, keyed by
registered name, unioning the agents each is on. Skills connections are excluded (they are
reported/handled separately). ``agents`` drops agents outside that scope, and a server left
with no in-scope agent is omitted. Each value is ``{"server", "clients", "managed"}``.

Shared by ``ug mcp list`` and ``ug mcp login`` so both see the same configured-server set."""
configured: dict[str, dict[str, Any]] = {}

def _collect(server: dict, *, managed: bool) -> None:
name = _server_name(server)
if not name or server.get("kind") == SKILLS_MCP_KIND:
return
clients = [
client for client in _mcp_server_clients(server) if agents is None or client in agents
]
if not clients:
return
entry = configured.setdefault(name, {"server": server, "clients": [], "managed": managed})
entry["clients"] = _merge_clients(entry["clients"], clients)
entry["managed"] = entry["managed"] or managed

for server in state.get("mcp_servers") or []:
_collect(server, managed=False)
for server in state.get("managed_mcp_servers") or []:
_collect(server, managed=True)
return configured


def list_mcp_command(agents: set[str] | None = None) -> int:
"""`ug mcp list`: show the Databricks MCP servers ug has configured and their live
connection status in each coding agent, one row per server.
Expand Down Expand Up @@ -2008,28 +2039,9 @@ def list_mcp_command(agents: set[str] | None = None) -> int:

live = _query_live_statuses(probe_clients)

# Merge developer- and workspace-managed servers by registered name, unioning their agents.
# ``--agents`` drops agents outside the scope, and a server left with no in-scope agent is
# omitted. The skills connection is intentionally excluded — it's reported by the skill commands.
configured: dict[str, dict[str, Any]] = {}

def _collect(server: dict, *, managed: bool) -> None:
name = _server_name(server)
if not name or server.get("kind") == SKILLS_MCP_KIND:
return
clients = [
client for client in _mcp_server_clients(server) if agents is None or client in agents
]
if not clients:
return
entry = configured.setdefault(name, {"server": server, "clients": [], "managed": managed})
entry["clients"] = _merge_clients(entry["clients"], clients)
entry["managed"] = entry["managed"] or managed

for server in state.get("mcp_servers") or []:
_collect(server, managed=False)
for server in state.get("managed_mcp_servers") or []:
_collect(server, managed=True)
# Merge developer- and workspace-managed servers by registered name, unioning their agents
# (shared with `ug mcp login` so both see the same configured-server set).
configured = configured_mcp_servers_by_name(state, agents)

if configured:
table = Table(box=None, pad_edge=False, header_style="bold")
Expand Down
Loading
Loading