Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ export const ErroredBorder: Story = {
},
};

// The failure reason kept on the errored card (#2783), so it is still there
// after the toast that first reported it auto-closes. A long transport error
// with a URL in it wraps inside the card rather than running under the border.
export const ErroredWithReason: Story = {
args: {
id: "5e4d3c2b-1a0f-4e9d-8c7b-6a5f4e3d2c1b",
name: "Legacy SSE Server",
config: { type: "sse", url: "https://mcp.example.com/sse" },
connection: disconnected,
errored: true,
errorMessage:
'Remote connect failed (500): {"error":"Failed to start transport: Endpoint origin does not match connection origin: http://10.0.0.5:8000"}',
},
};

// The green border flagging the server that just connected (#1682) — the
// success mirror of ErroredBorder. It persists while connected and clears on
// disconnect or a new connection attempt.
Expand Down
48 changes: 48 additions & 0 deletions clients/web/src/components/groups/ServerCard/ServerCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,54 @@ describe("ServerCard", () => {
});
});

describe("connection-failed reason (#2783)", () => {
const reason =
"Failed to start transport: Endpoint origin does not match connection origin: http://10.0.0.5:8000";

it("shows the failure reason on an errored card", () => {
renderWithMantine(
<ServerCard
{...baseProps}
connection={disconnected}
errored
errorMessage={reason}
/>,
);
expect(screen.getByRole("alert")).toHaveTextContent(reason);
});

it("shows the failure reason in compact mode too", () => {
renderWithMantine(
<ServerCard
{...baseProps}
connection={disconnected}
compact
errored
errorMessage={reason}
/>,
);
expect(screen.getByRole("alert")).toHaveTextContent(reason);
});

it("hides a stale reason once the card is no longer errored", () => {
renderWithMantine(
<ServerCard
{...baseProps}
connection={disconnected}
errorMessage={reason}
/>,
);
expect(screen.queryByRole("alert")).toBeNull();
});

it("renders no reason line for an errored card without a message", () => {
renderWithMantine(
<ServerCard {...baseProps} connection={disconnected} errored />,
);
expect(screen.queryByRole("alert")).toBeNull();
});
});

describe("connection-failed border (#1621)", () => {
it("draws the red errored-variant border when errored", () => {
const { container } = renderWithMantine(
Expand Down
18 changes: 18 additions & 0 deletions clients/web/src/components/groups/ServerCard/ServerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export interface ServerCardProps extends ServerEntry {
* is connected or a new connection is attempted.
*/
errored?: boolean;
/**
* Why the last connection attempt failed, shown under the header while
* `errored` is set (#2783). The failure toast auto-closes after a few
* seconds, which left a bare "Failed" with no reason once it had gone.
*/
errorMessage?: string;
/**
* When true, this server just connected successfully: the card draws the green
* highlight border and scrolls itself into view once the monitoring sidebar
Expand Down Expand Up @@ -108,6 +114,11 @@ const ProtocolText = Text.withProps({
ml: "auto",
});

const ConnectErrorText = Text.withProps({
variant: "connectError",
role: "alert",
});

const SubtleButton = Button.withProps({
variant: "subtle",
size: "xs",
Expand Down Expand Up @@ -173,6 +184,7 @@ export function ServerCard({
dragHandle,
highlighted = false,
errored = false,
errorMessage,
justConnected = false,
scrollOnHighlight = true,
onClearHighlight,
Expand Down Expand Up @@ -269,6 +281,12 @@ export function ServerCard({
</HeaderRight>
</HeaderRow>

{/* Shown in compact mode too: the reason is the one thing a failed card
has to say, and compact hides everything else below the header. */}
{errored && errorMessage && (
<ConnectErrorText>{errorMessage}</ConnectErrorText>
)}

{!compact && (
<>
<MetaRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@ describe("ServerListScreen", () => {
container.querySelectorAll('[data-variant="errored"]'),
).toHaveLength(0);
});

it("shows the failure reason only on the failed server's card (#2783)", () => {
renderWithMantine(
<ServerListScreen
{...baseProps}
erroredServerId="beta"
connectErrorMessage="Remote connect failed (500)"
/>,
);
const alerts = screen.getAllByRole("alert");
expect(alerts).toHaveLength(1);
expect(alerts[0]).toHaveTextContent("Remote connect failed (500)");
expect(alerts[0]!.closest('[data-variant="errored"]')).toHaveTextContent(
"Beta",
);
});
});

describe("just-connected highlight (#1682)", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface ServerListScreenProps {
* draws a red border until another server is connected or attempted.
*/
erroredServerId?: string;
/**
* Why that server's connection attempt failed (#2783), shown on the
* `erroredServerId` card so the reason outlasts the failure toast.
*/
connectErrorMessage?: string;
/**
* Id of the server that just connected successfully (#1682). Its card draws
* the green highlight border and scrolls into view — the success mirror of
Expand Down Expand Up @@ -190,6 +195,7 @@ export function ServerListScreen({
writable = true,
activeServer,
erroredServerId,
connectErrorMessage,
connectedServerId,
onAddManually,
onImportConfig,
Expand Down Expand Up @@ -252,6 +258,8 @@ export function ServerListScreen({
onRemove,
highlighted: highlightedServerIds?.includes(server.id) ?? false,
errored: server.id === erroredServerId,
errorMessage:
server.id === erroredServerId ? connectErrorMessage : undefined,
justConnected: server.id === connectedServerId,
scrollOnHighlight: server.id === firstHighlightedId,
onClearHighlight: onClearHighlight
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,7 @@ export function InspectorView({
writable={serverListWritable}
activeServer={dimCardsAgainst}
erroredServerId={erroredServerId}
connectErrorMessage={connectErrorMessage}
connectedServerId={connectedServerId}
onAddManually={onServerAdd}
onImportConfig={onServerImportConfig}
Expand Down
3 changes: 2 additions & 1 deletion clients/web/src/components/views/InspectorView/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ export interface ConnectionProps {
* Last connection-level error message (handshake failure, OAuth start
* failure, deep-link automation failure). Surfaced as `data-error-message`
* on the header's `connection-status` testid so an automated driver can read
* *why* a connect failed without scraping a transient toast.
* *why* a connect failed without scraping a transient toast, and — for the
* same reason, but for people — on the `erroredServerId` card (#2783).
*/
connectErrorMessage?: string;
initializeResult?: InitializeResult;
Expand Down
14 changes: 14 additions & 0 deletions clients/web/src/theme/Text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ export const ThemeText = Text.extend({
},
};
}
// The reason a server card's last connect attempt failed (#2783), kept on
// the card after the toast that first reported it auto-closes. The text
// is often a raw transport error carrying a URL or a JSON body, so it has
// to wrap inside the card rather than run under its border.
if (props.variant === "connectError") {
return {
root: {
color: "var(--inspector-danger-text)",
fontSize: "var(--mantine-font-size-sm)",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
},
};
}
// Two small, unobtrusive labels that sit in the footer row (#1682): the
// build version (left) and the copyright notice (right), positioned by the
// footer's `space-between` Group. Both are grey, single-line, and out of the
Expand Down
Loading