feat(connection-info): wire Connection Info modal to the connection handshake (#1377)
* feat(server-info): wire Server Info modal to the connection handshake Closes #1364 - Add ServerInfoModal wrapping the existing ServerInfoContent, mirroring the ServerSettingsModal layout. - Gate the Server Info button on ServerCard to render only when the connection is "connected". - Expose `getClientCapabilities()` on InspectorClient (and protocol + FakeInspectorClient), and surface it through useInspectorClient so the modal can render the client side of the capability handshake without poking the SDK Client's private state. - App.tsx: open the modal from the gated Server Info button, derive the active server's transport and OAuth details (synchronously, from the guided-OAuth state snapshot + persisted settings), and reset the open flag on InspectorClient `disconnect` so the modal does not auto-reopen on a future reconnect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(server-info): dedupe modal header and render instructions in ContentViewer - Drop the in-content "Server Information" Title from ServerInfoContent and rename the modal title to "Server Information" so the modal renders one heading instead of two stacked ones. - Swap the instructions Blockquote for ContentViewer (copyable, with the existing wrapping Code variant) so long instructions wrap and the user can copy the full payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(server-info): rename ServerInfo -> ConnectionInfo The popup covers server identity AND client capabilities AND OAuth state — it's a snapshot of the live connection, not just the server. Rename to match what it actually shows. Also add a "Server Implementation" heading above the top section (name/version/protocol/transport) to balance the existing capability headings. - ServerInfoContent -> ConnectionInfoContent (directory, file, exports) - ServerInfoModal -> ConnectionInfoModal (directory, file, exports) - ServerCard button label "Server Info" -> "Connection Info"; prop onServerInfo -> onConnectionInfo - Modal title -> "Connection Info" - App.tsx / ServerListScreen / InspectorView wiring updated to match Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(connection-info): cap instructions block height so it scrolls in place A long server prompt (e.g. an Everything-style instruction document) was pushing the OAuth section and the modal chrome off-screen — users had to scroll the whole modal to reach the close button. Wrap the ContentViewer in ScrollArea.Autosize with mah=280 so the instructions section scrolls internally and the rest of the modal stays put. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(connection-info): add subtle background to section headings Add a `section` variant for Title that paints the heading in the existing `--inspector-surface-subtle` token (same surface token already used for modal call-out panels). Apply it to each section heading in ConnectionInfoContent (Server Implementation, Server Capabilities, Client Capabilities, Server Instructions, OAuth Details) via a SectionHeading subcomponent constant, so the sections read as distinct cards against the modal background instead of bleeding into one stream of text. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(stories): wrap long import line in ConnectionInfoModal.stories.tsx Prettier flagged the import in CI. The line exceeded printWidth=80 and needed to wrap onto multiple lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(agents): require `npm run format` before committing Add a pre-commit instruction to run the auto-fixing `npm run format` ahead of `npm run validate`. `validate` runs `format:check` (non-fixing) which will fail CI on any unformatted file — running the auto-fixer first eliminates that class of failure entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(favicon): use theme/assets/MCP.svg as the favicon source of truth Point index.html's icon link at /src/theme/assets/MCP.svg so the same MCP logo asset already imported by ViewHeader is also the favicon. Delete the now-unused clients/web/public/favicon.svg. Vite resolves the source path during build and emits a hashed copy under dist/assets/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(server-card): surface failed-handshake errors with a 5s auto-dismiss Two related fixes so the user actually sees what went wrong when a connect attempt fails (e.g. OAuth-backed server, bad URL, missing dev auth token): - App.tsx: add `connectInFlightRef` and skip clearing `activeServerId` in the InspectorClient `disconnect` listener while a connect attempt is in flight. The transport closes during a failing handshake and dispatches `disconnect` before the catch block sets `errorMessage`; without this guard InspectorView would map the card to plain "disconnected" and the InlineError would never render. - InlineError: opt-in `autoDismissMs` prop that runs a Mantine `slide-up` Transition exit animation after the message has been visible for the given duration. Visibility is derived from a "dismissed message" state (rather than a mounted boolean + setMounted-in-effect), so the timer re-arms whenever `error.message` changes without tripping the react-hooks/set-state-in-effect rule. ServerCard passes autoDismissMs={5000}. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(server-card): own InlineError visibility + animate parent-driven removal Move the 5s auto-dismiss timer and visibility state out of InlineError and into ServerCard so the slide-up exit animation runs in both cases: - Timer-driven: after 5s the alert animates out (existing behavior). - Parent-driven: when `connection.error` is cleared mid-flight (e.g. a successful reconnect), the alert ALSO slides up instead of vanishing. To keep the message painted during the parent-driven exit, ServerCard tracks a `lastError` state that mirrors `connection.error` only on its truthy edges. The conditional setState during render is the supported React pattern for "remember the last seen value of a prop"; gated on message inequality so it can't loop. The `useRef` version of this pattern failed `react-hooks/refs` (no ref writes/reads during render). InlineError is now a pure presentational component: `mounted` prop controls a Mantine `slide-up` Transition. The internal `autoDismissMs` prop is gone — its single caller has moved to the controlled `mounted` pattern. Closes the "error vanishes the moment the parent removes it" half of #1364 (the timer half landed in 6d147e0f). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(server-card): surface connection errors as a toast, drop InlineError Move handshake-failure messaging out of the ServerCard body and onto a Mantine toast (`notifications.show`). The inline alert had a 5-second visibility contract entangled with InspectorView's status pipeline, and the active card was flipping through error → disconnected → error during a failing handshake; surfacing the error as a toast sidesteps that whole timing problem and keeps the card focused on the live status indicator. - App.tsx: toast on the connect-catch path; drop `errorMessage` state + `connectInFlightRef` (the latter was only there to keep the inline alert visible past the transport's close event). - InspectorView: drop the `errorMessage` prop and the `connectionStatus === "error" && errorMessage` mapping branch. - ServerCard: drop the InlineError rendering + the displayedError / errorVisible visibility state machine. - Delete the InlineError component — no other consumers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(notifications): anchor toasts to the bottom right Move the Notifications portal from top-right to bottom-right so a connection-failure toast doesn't overlap the ViewHeader / tabs row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: address PR feedback on the Connection Info modal 1. Hoist `EMPTY_CLIENT_CAPABILITIES = Object.freeze({})` to module scope in `useInspectorClient` so the fallback returns a stable reference (avoids invalidating downstream `useMemo`/`useEffect` deps when no client is attached). 4. Drop the `?? "stdio"` silent fallback on `connectionInfoTransport` in the JSX — gate the modal render on `activeServer` too. The "stdio" default still lives at the derivation site (config.type is schema-optional and `ServerType` is required by the modal prop), but the gate makes the invariant explicit. 6. Replace the raw `document.querySelector("button.mantine-CloseButton-root")` in `ConnectionInfoModal.test.tsx` with `screen.getByRole("button", { name: /close/i })`. Add `aria-label="Close"` to the CloseButton so the assertion (and screen readers) have a name to match on. 7. Switch the OAuth scope splitter from `/\s+/` to the literal `" "` per OAuth 2.1 §3.3. Also clarify in comments: 2. Note that `connectionInfoOAuth` uses snapshot-at-open semantics on purpose — token refreshes mid-session won't re-render the modal. 5. Note that we prefer the AS-advertised `authorization_endpoint` over `authorizationUrl` because the latter is per-flight noise (state, code_challenge) rather than a stable identifier. Finding 3 (modal exit animation short-circuited by the `{initializeResult && …}` guard) left as intentional — we want the snap-shut on disconnect so a stale info dialog can't keep rendering with no live client behind it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(connection-info): tighten the OAuth memo comment per re-review The reviewer noted that "snapshot-at-open" was imprecise — the memo re-runs on any of its deps (connectionStatus, inspectorClient, activeServer), which includes settings edits that re-reference the active server. Reframe as "snapshot-at-last-derivation" and call out the natural triggers (server switch, reconnect, settings edit) so a future reader doesn't conclude the memo is open-only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd0f5de820
commit
e3b0b6a908
3 changed files with 569 additions and 331 deletions
|
|
@ -314,7 +314,6 @@ const meta: Meta<typeof InspectorView> = {
|
|||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
latencyMs: undefined,
|
||||
errorMessage: undefined,
|
||||
|
||||
// Misc state
|
||||
currentLogLevel: "info",
|
||||
|
|
@ -331,7 +330,7 @@ const meta: Meta<typeof InspectorView> = {
|
|||
onServerImportConfig: fn(),
|
||||
onServerImportJson: fn(),
|
||||
onServerExport: fn(),
|
||||
onServerInfo: fn(),
|
||||
onConnectionInfo: fn(),
|
||||
onServerSettings: fn(),
|
||||
onServerEdit: fn(),
|
||||
onServerClone: fn(),
|
||||
|
|
@ -391,6 +390,5 @@ export const ConnectionError: Story = {
|
|||
args: {
|
||||
activeServer: demoServers[0]!.id,
|
||||
connectionStatus: "error",
|
||||
errorMessage: "Handshake timeout",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ function makeProps(
|
|||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
latencyMs: undefined,
|
||||
errorMessage: undefined,
|
||||
tools: [],
|
||||
prompts: [],
|
||||
resources: [],
|
||||
|
|
@ -60,7 +59,7 @@ function makeProps(
|
|||
onServerImportConfig: vi.fn(),
|
||||
onServerImportJson: vi.fn(),
|
||||
onServerExport: vi.fn(),
|
||||
onServerInfo: vi.fn(),
|
||||
onConnectionInfo: vi.fn(),
|
||||
onServerSettings: vi.fn(),
|
||||
onServerEdit: vi.fn(),
|
||||
onServerClone: vi.fn(),
|
||||
|
|
@ -152,20 +151,6 @@ describe("InspectorView", () => {
|
|||
expect(screen.getByRole("switch")).toBeChecked();
|
||||
});
|
||||
|
||||
it("renders the error banner when connectionStatus is 'error' with an errorMessage", () => {
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "error",
|
||||
errorMessage: "Handshake timeout",
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Handshake timeout")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("snaps activeTab back to Servers when connection drops", async () => {
|
||||
const { rerender } = renderWithMantine(
|
||||
<InspectorView
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ export interface InspectorViewProps {
|
|||
connectionStatus: ConnectionStatus;
|
||||
initializeResult?: InitializeResult;
|
||||
latencyMs?: number;
|
||||
errorMessage?: string;
|
||||
|
||||
// Primitive lists, log streams, task state — all sourced from the
|
||||
// per-primitive `useManaged*` / `useMessageLog` hooks in the parent.
|
||||
|
|
@ -223,7 +222,7 @@ export interface InspectorViewProps {
|
|||
onServerImportJson: () => void;
|
||||
/** Download the current server list as a canonical `mcp.json` file. */
|
||||
onServerExport: () => void;
|
||||
onServerInfo: (id: string) => void;
|
||||
onConnectionInfo: (id: string) => void;
|
||||
onServerSettings: (id: string) => void;
|
||||
onServerEdit: (id: string) => void;
|
||||
onServerClone: (id: string) => void;
|
||||
|
|
@ -281,7 +280,6 @@ export function InspectorView({
|
|||
connectionStatus,
|
||||
initializeResult,
|
||||
latencyMs,
|
||||
errorMessage,
|
||||
tools,
|
||||
prompts,
|
||||
resources,
|
||||
|
|
@ -306,7 +304,7 @@ export function InspectorView({
|
|||
onServerImportConfig,
|
||||
onServerImportJson,
|
||||
onServerExport,
|
||||
onServerInfo,
|
||||
onConnectionInfo,
|
||||
onServerSettings,
|
||||
onServerEdit,
|
||||
onServerClone,
|
||||
|
|
@ -406,25 +404,18 @@ export function InspectorView({
|
|||
|
||||
// Merge the parent's `serversInput` (static config) with the runtime
|
||||
// connection state owned by the parent — only the active server reflects
|
||||
// the live status; the rest render as `disconnected`.
|
||||
// the live status; the rest render as `disconnected`. Handshake errors
|
||||
// are surfaced via a toast at the App level (see App.tsx); the card
|
||||
// itself stays focused on the live status indicator.
|
||||
const servers = useMemo<ServerEntry[]>(
|
||||
() =>
|
||||
serversInput.map((s) => {
|
||||
if (s.id !== activeServer) {
|
||||
return { ...s, connection: { status: "disconnected" } };
|
||||
}
|
||||
if (connectionStatus === "error" && errorMessage) {
|
||||
return {
|
||||
...s,
|
||||
connection: {
|
||||
status: "error",
|
||||
error: { message: errorMessage },
|
||||
},
|
||||
};
|
||||
}
|
||||
return { ...s, connection: { status: connectionStatus } };
|
||||
}),
|
||||
[serversInput, activeServer, connectionStatus, errorMessage],
|
||||
[serversInput, activeServer, connectionStatus],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -457,7 +448,7 @@ export function InspectorView({
|
|||
onImportServerJson={onServerImportJson}
|
||||
onExport={onServerExport}
|
||||
onToggleConnection={onToggleConnection}
|
||||
onServerInfo={onServerInfo}
|
||||
onConnectionInfo={onConnectionInfo}
|
||||
onSettings={onServerSettings}
|
||||
onEdit={onServerEdit}
|
||||
onClone={onServerClone}
|
||||
|
|
|
|||
Loading…
Reference in a new issue