From 28c0c279aada5267d58ed668fade359d6b0ce019 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 11 Aug 2026 21:17:19 +0000 Subject: [PATCH] fix(ui): name MCP action switches Preserve the unverified-server trust marker on URL-only connection details and compact the mobile action step above the bottom nav. Co-Authored-By: Paperclip --- ui/src/pages/apps/AppDetail.test.tsx | 17 ++++++++ ui/src/pages/apps/AppDetail.tsx | 26 ++++++++++++ ui/src/pages/apps/AppsConnect.test.tsx | 25 ++++++++++- ui/src/pages/apps/AppsConnect.tsx | 47 +++++++++------------ ui/src/pages/apps/UnverifiedServerBadge.tsx | 14 ++++++ 5 files changed, 100 insertions(+), 29 deletions(-) create mode 100644 ui/src/pages/apps/UnverifiedServerBadge.tsx diff --git a/ui/src/pages/apps/AppDetail.test.tsx b/ui/src/pages/apps/AppDetail.test.tsx index 6be87c00cce..b0a8fbdc874 100644 --- a/ui/src/pages/apps/AppDetail.test.tsx +++ b/ui/src/pages/apps/AppDetail.test.tsx @@ -285,6 +285,23 @@ describe("AppDetail", () => { expect(updateConnectionMock).toHaveBeenCalledWith("conn-1", { enabled: false }); }); + it("keeps the unverified-server marker on URL-only connection details", async () => { + getConnectionMock.mockResolvedValue( + connection({ + name: "127.0.0.1", + config: { url: "http://127.0.0.1:8848/mcp" }, + transportConfig: { url: "http://127.0.0.1:8848/mcp" }, + }), + ); + + await renderAppDetail(); + + expect(container.textContent).toContain("Custom app"); + expect(container.textContent).toContain("hosted at 127.0.0.1"); + expect(container.textContent).toContain("Unverified server"); + expect(container.textContent).toContain("127.0.0.1:8848"); + }); + it("redirects a missing tab to setup", async () => { mockParams.tab = undefined; diff --git a/ui/src/pages/apps/AppDetail.tsx b/ui/src/pages/apps/AppDetail.tsx index d458947ceca..0621a5680dd 100644 --- a/ui/src/pages/apps/AppDetail.tsx +++ b/ui/src/pages/apps/AppDetail.tsx @@ -29,6 +29,7 @@ import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { cn } from "@/lib/utils"; import { AppLogo } from "./AppLogo"; +import { UnverifiedServerBadge } from "./UnverifiedServerBadge"; import { appDefinitionLogoUrl, appDefinitionName, @@ -523,6 +524,8 @@ function AppDetailHeader({ onRenameCancel: () => void; onRenameSubmit: (value: string) => void; }) { + const unverifiedHost = unverifiedRemoteHost(connection); + return (
@@ -567,6 +570,7 @@ function AppDetailHeader({ {connectionDisplaySecondaryHint(connection) && (

{connectionDisplaySecondaryHint(connection)}

)} + {unverifiedHost ? : null}
@@ -579,6 +583,28 @@ function AppDetailHeader({ ); } +function unverifiedRemoteHost(connection: ToolConnection): string | null { + const sourceTemplateKey = connection.config?.sourceTemplateKey ?? connection.transportConfig.sourceTemplateKey; + if ( + connection.transport !== "mcp_remote" + || (typeof sourceTemplateKey === "string" && sourceTemplateKey.trim()) + ) return null; + + const value = connection.config?.url + ?? connection.config?.endpoint + ?? connection.config?.remoteUrl + ?? connection.transportConfig.url + ?? connection.transportConfig.endpoint + ?? connection.transportConfig.remoteUrl; + if (typeof value !== "string") return null; + + try { + return new URL(value).host || null; + } catch { + return null; + } +} + type StatusInfo = { label: string; tone: "connected" | "attention" | "paused" }; function statusFor(connection: ToolConnection): StatusInfo { diff --git a/ui/src/pages/apps/AppsConnect.test.tsx b/ui/src/pages/apps/AppsConnect.test.tsx index aec5a95229d..fd6d8bce805 100644 --- a/ui/src/pages/apps/AppsConnect.test.tsx +++ b/ui/src/pages/apps/AppsConnect.test.tsx @@ -1169,8 +1169,28 @@ describe("AppsConnect — guided generic MCP flow (PAP-17087)", () => { isWrite: false, isDestructive: false, status: "active", + }, { + catalogEntryId: "cat-search", + toolName: "search_things", + title: "Search things", + description: null, + riskLevel: "read", + isReadOnly: true, + isWrite: false, + isDestructive: false, + status: "active", + }], + canMakeChanges: [{ + catalogEntryId: "cat-delete", + toolName: "qa_delete_widget", + title: null, + description: null, + riskLevel: "destructive", + isReadOnly: false, + isWrite: true, + isDestructive: true, + status: "active", }], - canMakeChanges: [], }, catalog: [], suggestedDefaults: { askFirstRiskLevels: ["write", "destructive"] }, @@ -1184,6 +1204,9 @@ describe("AppsConnect — guided generic MCP flow (PAP-17087)", () => { expect(container.textContent).toContain("List things"); expect(container.textContent).toContain("Unverified server"); expect(container.textContent).toContain("mcp.example.test"); + expect( + Array.from(container.querySelectorAll('[role="switch"]')).map((toggle) => toggle.getAttribute("aria-label")), + ).toEqual(["List things allowed", "Search things allowed", "qa_delete_widget allowed"]); }); it("offers a curated setup as a convenience without leaving the generic flow", async () => { diff --git a/ui/src/pages/apps/AppsConnect.tsx b/ui/src/pages/apps/AppsConnect.tsx index c4e3e484f50..f2c90595d14 100644 --- a/ui/src/pages/apps/AppsConnect.tsx +++ b/ui/src/pages/apps/AppsConnect.tsx @@ -11,7 +11,6 @@ import { Loader2, Lock, Search, - ShieldAlert, TerminalSquare, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; @@ -50,11 +49,13 @@ import { copyTextToClipboard } from "@/lib/clipboard"; import { resolveAuthorizationTarget } from "@/lib/authorizationUrl"; import { navigateTopLevel } from "@/lib/browserNavigation"; import { AppLogo } from "./AppLogo"; +import { UnverifiedServerBadge } from "./UnverifiedServerBadge"; import { appSourceConnectHref, isMcpDirectOAuthConnectSlug } from "./app-connect-policy"; import { parseGoogleSheetIds } from "./google-sheets"; import { canSubmitGenericConnect, customHeaderError, + defaultGenericMcpName, endpointHost, genericConnectGuidance, genericConnectPayload, @@ -680,7 +681,7 @@ export function AppsConnect() { const appName = connectResult?.application.name ?? entry?.name ?? - (linkName.trim() || defaultLinkName(linkUrl) || "this app"); + (linkName.trim() || defaultGenericMcpName(linkUrl) || "this app"); const zapierEntry = zapierSource ? galleryQuery.data?.apps.find((app) => app.slug === "zapier") ?? null : null; @@ -731,7 +732,7 @@ export function AppsConnect() { setEntry(null); setGalleryName(""); setLinkUrl(url); - setLinkName(matchedEntry?.name ?? defaultLinkName(url) ?? ""); + setLinkName(matchedEntry?.name ?? defaultGenericMcpName(url) ?? ""); setLinkNeedsKey(false); setLinkKey(""); setCredentials({}); @@ -1378,14 +1379,6 @@ function normalizeAppLink(value: string): string | null { } } -function defaultLinkName(link: string): string | null { - try { - return new URL(link).hostname.replace(/^www\./, ""); - } catch { - return null; - } -} - /** * The guided universal flow for an unknown remote MCP server (PAP-17087, plan 2). * @@ -1463,6 +1456,11 @@ function LinkConnectStep({ }; const canSubmit = canSubmitGenericConnect(draft); const showSimpleKeyQuestion = authMode === "auto"; + const nameInputRef = useRef(null); + + useEffect(() => { + if (guidance?.focus === "name") nameInputRef.current?.focus(); + }, [guidance]); const updateHeader = (id: string, patch: Partial) => { onHeadersChange(headers.map((row) => (row.id === id ? { ...row, ...patch } : row))); @@ -1505,6 +1503,7 @@ function LinkConnectStep({
onNameChange(e.target.value)} @@ -1717,18 +1716,6 @@ const GENERIC_AUTH_MODE_OPTIONS: Array<{ mode: GenericMcpAuthMode; label: string }, ]; -function UnverifiedServerBadge({ host, className }: { host: string | null; className?: string }) { - return ( -
- - - Unverified server - - {host ? {host} : null} -
- ); -} - function StoredSecurelyNote() { return (
@@ -2135,7 +2122,7 @@ function ActionGroup({ if (actions.length === 0) return null; return (
-
+
{title} · {hint} @@ -2153,7 +2140,7 @@ function ActionGroup({ const on = enabled[action.catalogEntryId] ?? false; const showAskFirst = on && askFirstLevels.includes(action.riskLevel); return ( -
+
{action.title ?? action.toolName} @@ -2167,7 +2154,11 @@ function ActionGroup({ Ask first )} - onToggle(action.catalogEntryId, next)} /> + onToggle(action.catalogEntryId, next)} + />
); })} @@ -2199,7 +2190,7 @@ function ActionsStep({ const enabledCount = Object.values(enabled).filter(Boolean).length; return ( -
+
@@ -2236,7 +2227,7 @@ function ActionsStep({ askFirstLevels={askFirstLevels} /> -
+
diff --git a/ui/src/pages/apps/UnverifiedServerBadge.tsx b/ui/src/pages/apps/UnverifiedServerBadge.tsx new file mode 100644 index 00000000000..31e7185cebf --- /dev/null +++ b/ui/src/pages/apps/UnverifiedServerBadge.tsx @@ -0,0 +1,14 @@ +import { ShieldAlert } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export function UnverifiedServerBadge({ host, className }: { host: string | null; className?: string }) { + return ( +
+ + + Unverified server + + {host ? {host} : null} +
+ ); +}