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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,33 @@ profile. V2 AI Gateway servers can be added with typed selectors such as
`vector-search:main.docs`, `uc-functions:main.tools`, `external:<name>`,
`genie-space:<space-id>`, or `app:<name>`.

Sign in to connection-backed servers with `ug mcp login`:

```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
```

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. `ug mcp login`
uses the same configured-server set as `ug mcp list` (including servers delivered through the
agents' OS-managed files), 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

Unity Catalog Skills can be registered as MCP tools or downloaded into local
Expand Down Expand Up @@ -130,6 +157,7 @@ ug skills remove --skill main.default.my-skill
| `ug mcp add` | Add MCP servers without removing existing registrations |
| `ug mcp remove` | Unregister configured MCP servers |
| `ug mcp list` | List configured MCP servers and connection status |
| `ug mcp login` | Sign in to connection-backed MCP services (interactive, or `--services`) |
| `ug skills list` | List configured skills and how each was configured |
| `ug skills add` | Add skill MCP scopes or download skills |
| `ug skills remove` | Remove skill MCP scopes or downloaded skills |
Expand Down
44 changes: 44 additions & 0 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,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 @@ -1335,6 +1336,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
74 changes: 45 additions & 29 deletions src/ucode/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2112,6 +2112,47 @@ 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"}``.

Managed servers can be delivered two ways: to fallback state (``managed_mcp_servers``) or, for
Claude/Codex, into the agents' OS-managed files — the latter is the source of truth, so it is
read directly here. Shared by ``ug mcp list`` and ``ug mcp login`` so both (and ``ug status``)
see the same configured-server set regardless of how a managed server was delivered."""
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)
# Managed servers delivered through the agents' OS-managed files (Claude/Codex) live in those
# files, not in state, so read them too — otherwise `ug mcp login` would miss them.
for agent, module in (("claude", claude), ("codex", codex)):
if agents is not None and agent not in agents:
continue
for name, url in module.read_managed_mcp_urls().items():
_collect({"name": name, "url": url, "clients": [agent]}, 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 @@ -2143,35 +2184,10 @@ 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)
# Managed servers also live in the OS-managed files (source of truth), not just fallback state.
for server in state.get("managed_mcp_servers") or []:
_collect(server, managed=True)
if agents is None or "claude" in agents:
for name, url in claude.read_managed_mcp_urls().items():
_collect({"name": name, "url": url, "clients": ["claude"]}, managed=True)
if agents is None or "codex" in agents:
for name, url in codex.read_managed_mcp_urls().items():
_collect({"name": name, "url": url, "clients": ["codex"]}, 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, including the servers
# delivered through the agents' OS-managed files).
configured = configured_mcp_servers_by_name(state, agents)

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