diff --git a/README.md b/README.md
index d0a457c..caed922 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,26 @@ ucode configure --agents claude --mcp system.ai.slack
`--mcp` also works without `--agents` for MCP-only clients (it configures just the workspace,
then registers the servers); pass a comma-separated list to register several at once.
+#### Add servers without replacing existing ones
+
+`ucode configure mcp` **replaces** the registered MCP servers with your selection — anything
+outside a `--location`/`--services` scope (or left unchecked in the picker) is removed. To
+**add** servers while leaving everything already configured in place, use `ucode mcp add`:
+
+```bash
+# Register a whole schema's services, keeping any servers already configured.
+ucode mcp add --location system.ai
+
+# Register just a subset (same name rules as `configure mcp --services`).
+ucode mcp add --services system.ai.slack,system.ai.github
+
+# No arguments launches the same interactive picker, but never removes servers.
+ucode mcp add
+```
+
+`ucode mcp add` takes the same `--location` and `--services` options as `ucode configure mcp`;
+the only difference is that it never removes servers outside the selection.
+
### Skills (optional)
Configure Unity Catalog Skills for your coding tools with `ucode configure skills`:
@@ -243,6 +263,8 @@ pick the new config up on their next ucode run.
| `ucode configure --skip-validate` | Write configs without sending a test message through each agent |
| `ucode configure --agents claude,codex,pi --skip-unavailable` | Configure the requested agents that are available; skip the rest with a warning |
| `ucode configure --agents claude --mcp system.ai.slack` | Configure an agent and register its Databricks MCP server(s) in one command |
+| `ucode mcp add --location system.ai` | Register a schema's MCP servers, keeping any already configured (additive; never removes) |
+| `ucode mcp add --services system.ai.slack` | Register specific MCP server(s) without removing existing ones |
| `ucode configure skills` | Register the skills MCP connection (utility tools only); no skills download |
| `ucode configure skills --location main.default [--path
]` | Download a schema's skills to disk (under ``, or your home dir) and register a schema-less skills MCP connection |
| `ucode configure skills --location main.default --skill my-skill` | Download only the named skill(s) from a schema (comma-separated for several) |
diff --git a/src/ucode/cli.py b/src/ucode/cli.py
index c2e38f4..3ddb881 100644
--- a/src/ucode/cli.py
+++ b/src/ucode/cli.py
@@ -84,6 +84,7 @@
from ucode.mcp import (
MCP_CLIENTS,
SKILLS_MCP_KIND,
+ add_mcp_command,
apply_managed_mcp_servers,
apply_managed_skills,
configure_mcp_command,
@@ -1056,6 +1057,43 @@ def _version_callback(value: bool) -> None:
raise typer.Exit()
+@mcp_app.command("add")
+def mcp_add(
+ location: Annotated[
+ str | None,
+ typer.Option(
+ "--location",
+ help="Non-interactive: register the MCP services in the given Unity Catalog "
+ "`.` (e.g. `system.ai`) and exit without showing the picker. "
+ "Servers already configured outside this location are kept.",
+ ),
+ ] = None,
+ services: Annotated[
+ str | None,
+ typer.Option(
+ "--services",
+ help="Register exactly this comma-separated subset of MCP services. Full names like "
+ "`system.ai.github` work on their own; bare short names like `github` need --location "
+ "to locate them. Omit --services to register the whole --location schema.",
+ ),
+ ] = None,
+) -> None:
+ """Add Databricks MCP servers to installed coding tools.
+
+ Like `ucode configure mcp`, but purely additive: it never removes MCP servers
+ that are already configured, only registers new ones.
+ """
+ selected = None if services is None else {s.strip() for s in services.split(",") if s.strip()}
+ try:
+ add_mcp_command(location=location, services=selected)
+ 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."""
diff --git a/src/ucode/mcp.py b/src/ucode/mcp.py
index a3db790..5602d4d 100644
--- a/src/ucode/mcp.py
+++ b/src/ucode/mcp.py
@@ -1739,15 +1739,43 @@ def setup_mcp_clients(state: dict, section: str) -> tuple[str, str | None, list[
return workspace, profile, clients
+def _union_missing(base: list[dict], selected: list[dict]) -> list[dict]:
+ """Return ``selected`` followed by every ``base`` server whose name isn't
+ already in it. Used by ``ucode mcp add`` so registering new servers never
+ removes ones that are already configured (append semantics)."""
+ have = _servers_by_name(selected)
+ extra = [s for s in base if (_server_name(s) or "") not in have]
+ return [*selected, *extra]
+
+
+def add_mcp_command(
+ location: str | None = None,
+ services: set[str] | None = None,
+) -> int:
+ """`ucode mcp add`: register Databricks MCP servers WITHOUT removing any that
+ are already configured.
+
+ Uses the same discovery and options as `configure mcp` — the interactive
+ picker, or the non-interactive `--location`/`--services` paths — but is purely
+ additive: unlike `configure mcp`, it never removes servers outside the
+ selection."""
+ return configure_mcp_command(location=location, services=services, append=True)
+
+
def configure_mcp_command(
location: str | None = None,
services: set[str] | None = None,
*,
exclude_sources: set[str] | None = None,
+ append: bool = False,
) -> int:
"""Interactive MCP picker. ``exclude_sources`` hides search sources the caller can't use —
`ucode setup` passes ``{"apps"}`` because a managed config can't carry an app's off-workspace
- host, so an app picked here would be silently dropped from the published config."""
+ host, so an app picked here would be silently dropped from the published config.
+
+ ``append`` (used by `ucode mcp add`) makes the command purely additive: the
+ final server list is unioned with the already-configured servers, so nothing
+ outside the current selection is removed."""
if services is not None and location is None:
# `--services` works standalone with full names (`system.ai.github`): the
# `.` to configure is derived from them. Bare short names
@@ -1766,13 +1794,19 @@ def configure_mcp_command(
)
location = next(iter(schemas))
state = load_state()
- workspace, profile, clients = setup_mcp_clients(state, "MCP Servers")
+ workspace, profile, clients = setup_mcp_clients(
+ state, "Add MCP Servers" if append else "MCP Servers"
+ )
original_mcp_servers_for_location: list[dict] = list(state.get("mcp_servers") or [])
if location is not None:
working_mcp_servers = _resolve_location_mcp_servers(
workspace, profile, clients, location, original_mcp_servers_for_location, services
)
+ if append:
+ working_mcp_servers = _union_missing(
+ original_mcp_servers_for_location, working_mcp_servers
+ )
changed = apply_mcp_server_changes(
original_mcp_servers_for_location,
working_mcp_servers,
@@ -1866,6 +1900,9 @@ def configure_mcp_command(
)
working_names.add(entry_name)
+ if append:
+ working_mcp_servers = _union_missing(original_mcp_servers, working_mcp_servers)
+
changed = apply_mcp_server_changes(
original_mcp_servers,
working_mcp_servers,
@@ -1878,7 +1915,8 @@ def configure_mcp_command(
state["mcp_servers"] = working_mcp_servers
save_state(state)
added = sorted(working_names - set(original_by_name))
- removed = sorted(set(original_by_name) - working_names)
+ # `add` never removes; the union above re-keeps unselected servers.
+ removed = [] if append else sorted(set(original_by_name) - working_names)
print_success(_mcp_change_summary(added, removed, clients))
elif not selections and not original_mcp_servers:
# User submitted the picker without toggling anything --> make it clear nothing was selected
diff --git a/tests/test_mcp.py b/tests/test_mcp.py
index 2852f47..002a0f8 100644
--- a/tests/test_mcp.py
+++ b/tests/test_mcp.py
@@ -1806,6 +1806,92 @@ def test_existing_entry_gets_reconfigured_for_newly_added_clients(self, monkeypa
]
+class TestAddMcpCommand:
+ """`ucode mcp add` (append) registers new servers without removing existing ones."""
+
+ def test_keeps_servers_outside_location(self, monkeypatch):
+ """Unlike `configure mcp --location`, `mcp add --location` preserves any
+ server outside the location instead of removing it."""
+ saved_states: list[dict] = []
+ configured: list[tuple[str, str, str]] = []
+ removed: list[tuple[str, str]] = []
+ outside_entry = {
+ "name": "databricks-sql",
+ "url": f"{WS}/api/2.0/mcp/sql",
+ "auth": "proxy",
+ "clients": ["claude"],
+ }
+ _stub_location_base(
+ monkeypatch,
+ {**CLAUDE_STATE, "mcp_servers": [outside_entry]},
+ )
+ monkeypatch.setattr(
+ mcp,
+ "list_mcp_services",
+ lambda workspace, token, parent: (["system.ai.github"], None),
+ )
+ monkeypatch.setattr(
+ mcp,
+ "configure_client_mcp_server",
+ lambda client, name, url, *a, **kw: configured.append((client, name, url)) or [],
+ )
+ monkeypatch.setattr(
+ mcp,
+ "remove_client_mcp_server",
+ lambda client, name: removed.append((client, name)) or [],
+ )
+ monkeypatch.setattr(mcp, "save_state", lambda state: saved_states.append(state.copy()))
+
+ assert mcp.add_mcp_command(location="system.ai") == 0
+
+ # Nothing is removed; the new service is added and the outside one kept.
+ assert removed == []
+ assert [c[1] for c in configured] == ["system-ai-github"]
+ assert saved_states[-1]["mcp_servers"] == [
+ {
+ "name": "system-ai-github",
+ "url": f"{WS}/ai-gateway/mcp-services/system.ai.github",
+ "auth": "proxy",
+ "clients": ["claude"],
+ },
+ outside_entry,
+ ]
+
+ def test_services_subset_keeps_others_in_location(self, monkeypatch):
+ """`mcp add --services` registers the named subset while leaving other
+ already-registered services in the same schema untouched."""
+ saved_states: list[dict] = []
+ removed: list[tuple[str, str]] = []
+ existing = {
+ "name": "system-ai-slack",
+ "url": f"{WS}/ai-gateway/mcp-services/system.ai.slack",
+ "auth": "proxy",
+ "clients": ["claude"],
+ }
+ _stub_location_base(
+ monkeypatch,
+ {**CLAUDE_STATE, "mcp_servers": [existing]},
+ )
+ monkeypatch.setattr(
+ mcp,
+ "list_mcp_services",
+ lambda workspace, token, parent: (["system.ai.github", "system.ai.slack"], None),
+ )
+ monkeypatch.setattr(mcp, "configure_client_mcp_server", lambda *a, **kw: [])
+ monkeypatch.setattr(
+ mcp,
+ "remove_client_mcp_server",
+ lambda client, name: removed.append((client, name)) or [],
+ )
+ monkeypatch.setattr(mcp, "save_state", lambda state: saved_states.append(state.copy()))
+
+ assert mcp.add_mcp_command(location="system.ai", services={"github"}) == 0
+
+ assert removed == []
+ names = [s["name"] for s in saved_states[-1]["mcp_servers"]]
+ assert names == ["system-ai-github", "system-ai-slack"]
+
+
class TestConfigureMcpServicesSubset:
"""`--location --services a,b,...` configures exactly the named subset."""