fix(web): render the connected header when a modern server omits serverInfo (#1773)

A modern-era server/discover makes serverInfo optional (SHOULD, not MUST); a conforming modern server that omits it left initializeResult undefined, collapsing the entire connected header + tab bar so the server was uninspectable. Gate initializeResult on connectionStatus alone with a catalog-name fallback, and thread a serverInfoReported flag so the Connection Info modal reports the omission truthfully instead of presenting the inferred name as server-sent. Closes #1772.
This commit is contained in:
Cliff Hall 2026-07-25 11:40:31 -04:00 • committed by GitHub
commit 820d38c438
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1473,21 +1473,47 @@ function App() {
};
}, [inspectorClient]);
// The Server Info modal needs the active server's transport and (optional)
// OAuth details — both are co-located here so the modal opens against the
// same connection snapshot the header is reading. Also feeds the
// `initializeResult` serverInfo fallback below.
const activeServer = useMemo<ServerEntry | undefined>(
() => servers.find((s) => s.id === activeServerId),
[servers, activeServerId],
);
// Whether the server actually reported `serverInfo`, vs. the catalog-name
// fallback synthesized below. Threaded to the Connection Info modal so it can
// show "not reported" instead of an inferred name that looks server-sent.
const serverInfoReported = serverInfo !== undefined;
// Build the InitializeResult the connected ViewHeader / Connection Info
// modal expect from the hook's split fields. `protocolVersion` is the value
// the InspectorClient negotiated during initialize (#1324); it's dispatched
// alongside serverInfo, so in practice it's present whenever we're connected.
// We deliberately gate only on serverInfo (not protocolVersion): this object
// also drives the connected header and Connection Info modal, so a
// missing/edge-case version must not hide those. It flows through as the
// optional field it is everywhere downstream (the ServerCard label and the
// modal value both tolerate an empty string), so "" reads as "unknown".
// We gate only on `connectionStatus`, never on serverInfo or protocolVersion:
// this object also drives the connected header (and its whole tab bar) and the
// Connection Info modal, so a missing field must not hide those.
//
// A modern-era server's `server/discover` makes `serverInfo` OPTIONAL (SHOULD,
// not MUST — it's stamped in `_meta["io.modelcontextprotocol/serverInfo"]`), so
// a conforming modern server may omit it and `serverInfo` stays undefined even
// while connected. Falling back to the catalog name (rather than returning
// `undefined`) keeps the header + tabs rendered for those servers (#1772). It
// fires for such a modern server — and, harmlessly, in the batched instant on
// any connect (legacy included) between the `connected` status dispatch and the
// `serverInfo` dispatch, which land in a single React render. The modal uses
// `serverInfoReported` (above), not this name, to stay faithful.
const initializeResult = useMemo<InitializeResult | undefined>(() => {
if (connectionStatus !== "connected" || !serverInfo) return undefined;
if (connectionStatus !== "connected") return undefined;
const resolvedServerInfo = serverInfo ?? {
name: activeServer?.name ?? "",
version: "",
};
return {
protocolVersion: protocolVersion ?? "",
capabilities: capabilities ?? {},
serverInfo,
serverInfo: resolvedServerInfo,
...(instructions ? { instructions } : {}),
};
}, [
@ -1496,16 +1522,9 @@ function App() {
serverInfo,
instructions,
protocolVersion,
activeServer,
]);
// The Server Info modal needs the active server's transport and (optional)
// OAuth details — both are co-located here so the modal opens against the
// same connection snapshot the header is reading.
const activeServer = useMemo<ServerEntry | undefined>(
() => servers.find((s) => s.id === activeServerId),
[servers, activeServerId],
);
// Mirror the active server's name into a ref so a mid-session failure toast
// can still name the server: a transport crash dispatches `disconnect`,
// which clears `activeServerId` (and thus `activeServer`) before the
@ -4448,6 +4467,7 @@ function App() {
opened={connectionInfoModalOpen}
onClose={() => setConnectionInfoModalOpen(false)}
initializeResult={initializeResult}
serverInfoReported={serverInfoReported}
clientCapabilities={clientCapabilities}
transport={connectionInfoTransport}
protocolEra={protocolEra}