diff --git a/surfaces/gui/e2e/mcp-add-test.spec.ts b/surfaces/gui/e2e/mcp-add-test.spec.ts
index 9f8a8e3e..c429d03f 100644
--- a/surfaces/gui/e2e/mcp-add-test.spec.ts
+++ b/surfaces/gui/e2e/mcp-add-test.spec.ts
@@ -80,3 +80,18 @@ test("JSON tab adds stdio as Not tested; detail Test flips it to Live", async ({
await detail.getByTestId("mcp-remove-files").click();
await expect(page.getByTestId("mcp-row-files")).toHaveCount(0);
});
+
+test("the name field prefills from the URL's distinctive host label", async ({ page }) => {
+ await openConnectors(page);
+ await page.getByTestId("add-custom-server").click();
+ const modal = page.getByTestId("add-mcp-modal");
+
+ // Generic labels (mcp/api/data/www) are skipped; the first distinctive one wins.
+ await modal.getByTestId("mcp-add-url").fill("https://data.dlai.link/api/mcp");
+ await expect(modal.getByTestId("mcp-add-name")).toHaveValue("dlai");
+
+ // Never overwrite what the user typed.
+ await modal.getByTestId("mcp-add-name").fill("warehouse");
+ await modal.getByTestId("mcp-add-url").fill("https://mcp.linear.app/mcp");
+ await expect(modal.getByTestId("mcp-add-name")).toHaveValue("warehouse");
+});
diff --git a/surfaces/gui/src/components/connectors/ConnectorsList.tsx b/surfaces/gui/src/components/connectors/ConnectorsList.tsx
index 7dac8cda..8657d729 100644
--- a/surfaces/gui/src/components/connectors/ConnectorsList.tsx
+++ b/surfaces/gui/src/components/connectors/ConnectorsList.tsx
@@ -49,7 +49,7 @@ export function ConnectorsList({
onClick={() => setAddingMcp(true)}
data-testid="add-custom-server"
>
- + Add custom server
+ + Add custom MCP server
void;
onChanged: () => void;
}) {
+ const servers = serversProp;
+ // A probe in flight ("Testing…" / "Signing in…") settles server-side within seconds,
+ // but this page has no standing MCP poll — the chip froze on Testing forever
+ // (owner-hit 2026-08-21, add-by-URL against a guarded server). While any row is
+ // authorizing, poll the parent's refresh until every row settles.
+ const anyAuthorizing = servers.some((s) => s.status === "authorizing");
+ useEffect(() => {
+ if (!anyAuthorizing) return;
+ const t = setInterval(onChanged, 1000);
+ return () => clearInterval(t);
+ }, [anyAuthorizing, onChanged]);
const presets = MCP_PRESETS.filter((p) => !servers.some((s) => s.name === p.name));
if (servers.length === 0 && presets.length === 0) return null;
@@ -157,6 +168,24 @@ const EXAMPLE = `{
const INPUT =
"w-full text-[13px] px-3 py-2 rounded-lg border border-line bg-paper text-ink outline-none focus:border-accent";
+// A friendly default server name from its URL: walk the hostname's labels left to
+// right, skip the generic ones (mcp/api/data/www…), take the first distinctive label
+// (mcp.linear.app → "linear", data.dlai.link → "dlai"); fall back to the 2nd-level
+// domain. The user can always overtype it.
+const GENERIC_LABELS = new Set(["www", "mcp", "api", "data", "remote", "server", "agent", "app"]);
+function nameFromUrl(raw: string): string {
+ try {
+ const host = new URL(raw).hostname.toLowerCase();
+ const labels = host.split(".").filter(Boolean);
+ if (labels.length < 2) return "";
+ const candidates = labels.slice(0, -1); // drop the TLD
+ const pick = candidates.find((l) => !GENERIC_LABELS.has(l)) || candidates[candidates.length - 1];
+ return pick.replace(/[^a-z0-9-]/g, "");
+ } catch {
+ return "";
+ }
+}
+
export function AddMcpModal({
onClose,
onChanged,
@@ -258,7 +287,12 @@ export function AddMcpModal({
/>
setUrl(e.target.value)}
+ onChange={(e) => {
+ const u = e.target.value;
+ setUrl(u);
+ // Prefill the name once the URL looks real — never overwrite typing.
+ if (!name.trim()) setName(nameFromUrl(u));
+ }}
placeholder="https://mcp.example.com/mcp"
spellCheck={false}
className={INPUT + " font-mono text-[12px]"}