MCP add flow: poll while a probe is in flight; name prefills from the URL

The Testing chip froze forever on guarded servers — the page never refetched
after the probe settled server-side. Opener renamed to Add custom MCP server.
This commit is contained in:
Rohit C Prasad
2026-08-21 18:25:57 -07:00
parent 2733eb84ca
commit e247997a14
3 changed files with 52 additions and 3 deletions
+15
View File
@@ -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");
});
@@ -49,7 +49,7 @@ export function ConnectorsList({
onClick={() => setAddingMcp(true)}
data-testid="add-custom-server"
>
+ Add custom server
+ Add custom MCP server
</button>
<input
placeholder="Search"
@@ -88,7 +88,7 @@ function McpGlyph() {
}
export function CustomMcpGroup({
servers,
servers: serversProp,
onOpen,
onChanged,
}: {
@@ -96,6 +96,17 @@ export function CustomMcpGroup({
onOpen: (name: string) => 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({
/>
<input
value={url}
onChange={(e) => 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]"}