Merge branch 'v2/main' into v2/feat/2068-refresh-token-toggle

This commit is contained in:
Cliff Hall 2026-08-25 00:08:40 -04:00 • committed by GitHub
commit 40cfb51f70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 493 additions and 43 deletions

View file

@ -1,4 +1,4 @@
import { Alert, Button, Group, Text } from "@mantine/core";
import { Alert, Button, Group, Stack, Text } from "@mantine/core";
export const DEFAULT_REAUTH_BANNER_TITLE = "Re-authentication required";
export const DEFAULT_REAUTH_BANNER_ACTION_LABEL = "Re-authenticate";
@ -24,11 +24,16 @@ const ReAuthAlert = Alert.withProps({
closeButtonLabel: "Dismiss",
});
const BannerRow = Group.withProps({
justify: "space-between",
align: "center",
wrap: "nowrap",
gap: "md",
// Message above the action rather than beside it: this renders in a ~420px
// popup, where a side-by-side row squeezes the message into a few words per
// line while the button keeps its full width.
const BannerBody = Stack.withProps({
gap: "xs",
});
const BannerActions = Group.withProps({
justify: "flex-end",
gap: "xs",
});
const MessageText = Text.withProps({
@ -50,10 +55,12 @@ export function ReAuthBanner({
}: ReAuthBannerProps) {
return (
<ReAuthAlert title={title} onClose={onDismiss}>
<BannerRow>
<BannerBody>
<MessageText>{message}</MessageText>
<ReAuthButton onClick={onReauthenticate}>{actionLabel}</ReAuthButton>
</BannerRow>
<BannerActions>
<ReAuthButton onClick={onReauthenticate}>{actionLabel}</ReAuthButton>
</BannerActions>
</BannerBody>
</ReAuthAlert>
);
}

View file

@ -1689,6 +1689,87 @@ describe("InspectorView", () => {
expect(screen.queryByRole("radio", { name: "Console" })).toBeNull();
});
// An OAuth failure is torn down by the parent, so the session rests at
// "disconnected" rather than "error" — the status gate the failure column
// used to carry never saw it, and the requests explaining the failure were
// unreachable (#2108). The parent's `erroredServerId` is the signal instead.
it("opens the monitoring sidebar to Network when OAuth fails without an error status (#2108)", async () => {
const { rerender } = renderWithMantine(
<StatefulInspectorViewHost
{...makeProps({
servers: [httpServer],
activeServer: "beta",
connectionStatus: "connecting",
})}
/>,
);
expect(
screen.queryByRole("button", { name: "Close monitoring sidebar" }),
).toBeNull();
rerender(
<StatefulInspectorViewHost
{...makeProps({
servers: [httpServer],
// The failure's teardown clears the active server and settles the
// session at "disconnected" — never "error".
activeServer: undefined,
erroredServerId: "beta",
connectionStatus: "disconnected",
initializeResult: undefined,
network: [
{
id: "a1",
timestamp: new Date(),
method: "GET",
url: "https://as.example.com/.well-known/oauth-authorization-server",
requestHeaders: {},
responseStatus: 404,
responseStatusText: "Not Found",
category: "auth",
},
],
})}
/>,
);
expect(
await screen.findByRole("button", { name: "Close monitoring sidebar" }),
).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Network" })).toBeChecked();
});
it("does not re-open the sidebar on a mount that starts already flagged (#2108)", () => {
// Same props as the failure above, but as the *initial* render: there is
// no transition, so a user who closed the column isn't fought on remount.
renderWithMantine(
<StatefulInspectorViewHost
{...makeProps({
servers: [httpServer],
activeServer: undefined,
erroredServerId: "beta",
connectionStatus: "disconnected",
initializeResult: undefined,
network: [
{
id: "a1",
timestamp: new Date(),
method: "GET",
url: "https://as.example.com/.well-known/oauth-authorization-server",
requestHeaders: {},
responseStatus: 404,
responseStatusText: "Not Found",
category: "auth",
},
],
})}
/>,
);
expect(
screen.queryByRole("button", { name: "Close monitoring sidebar" }),
).toBeNull();
});
it("surfaces Console (stderr), not Network, in the failure column for a stdio server (#1621)", async () => {
const stdioErr: ServerEntry = {
id: "beta",

View file

@ -388,6 +388,13 @@ export interface InspectorViewProps {
* the Servers screen draws a red border until another server is connected or
* a new connection is attempted. Independent of `activeServer`, which the
* parent clears on the failure's `disconnect` event.
*
* It is also the sole signal that opens the monitoring sidebar onto the
* failure's diagnostics (#2108). That used to be gated on the `"error"`
* connection status, which an OAuth failure never reaches — the parent drives
* that leg and tears the client down itself, settling the session at
* `"disconnected"`. So the parent must set this for *every* connect-attempt
* failure, auth legs included, and clear it as each new attempt starts.
*/
erroredServerId?: string;
/**
@ -817,18 +824,33 @@ export function InspectorView({
// connecting/disconnected → error, never connected → error. This keeps the
// auto-open aligned with the red-border (`erroredServerId`), which the parent
// also sets only for connect-attempt failures.
//
// A connect attempt that fails *during OAuth* (#2108) never reaches the
// `"error"` status at all: the auth leg is driven from the parent, which
// tears the client down on failure, so the session settles at
// `"disconnected"`. So the failure arm also fires on the parent raising
// `erroredServerId` — the canonical "this server's last connect attempt
// failed" flag, which the parent clears at the start of every new attempt.
// Tracked through a ref for the same reason as the status: a mount that
// starts already flagged must not re-open a column the user closed.
const prevStatusRef = useRef(connectionStatus);
const prevErroredServerIdRef = useRef(erroredServerId);
useEffect(() => {
const prev = prevStatusRef.current;
prevStatusRef.current = connectionStatus;
const prevErrored = prevErroredServerIdRef.current;
prevErroredServerIdRef.current = erroredServerId;
const becameConnected =
connectionStatus === "connected" && prev !== "connected";
const becameError =
connectionStatus === "error" && prev !== "error" && prev !== "connected";
(connectionStatus === "error" &&
prev !== "error" &&
prev !== "connected") ||
(erroredServerId !== undefined && prevErrored !== erroredServerId);
if (becameConnected || becameError) {
setMonitorPinned(true);
}
}, [connectionStatus, setMonitorPinned]);
}, [connectionStatus, erroredServerId, setMonitorPinned]);
const appTools = useMemo<Tool[]>(() => {
return tools.filter((tool) => {
@ -924,7 +946,14 @@ export function InspectorView({
// (set by the parent only for connect-attempt failures, not mid-session
// crashes) so a crash of a previously-connected server doesn't reorganize the
// column into the failure tab set — matching the auto-open effect above.
const failed = connectionStatus === "error" && erroredServerId !== undefined;
//
// Keyed on `erroredServerId` alone rather than on the `"error"` status
// (#2108): an OAuth leg that fails is torn down by the parent, so the session
// rests at `"disconnected"` and a status gate would never see the failure —
// which is exactly the case a server developer needs the Network tab for.
// `!connected` is the only status condition left, and it holds vacuously
// whenever the flag is set (the parent clears it as each new attempt starts).
const failed = !connected && erroredServerId !== undefined;
const monitorAvailable = useMemo<string[]>(() => {
if (connected) return availableTabs.filter((t) => MONITOR_TABS.includes(t));
if (failed) {