* feat(web): show negotiated MCP protocol version on ServerCard (#1324) Add optional protocolVersion to ConnectionState and render it as a dimmed "MCP <version>" label at the end of the transport-badge/ description row on ServerCard, shown only when connected. The value is plumbed through useInspectorClient as part of #1324. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(core,web): surface negotiated protocolVersion end-to-end (#1324) Plumb the MCP protocol version negotiated during initialize from the InspectorClient through to the ServerCard so the active connected card displays "MCP <version>". The SDK Client doesn't expose protocolVersion; it stamps it onto the transport via setProtocolVersion. MessageTrackingTransport now captures it (and forwards to the base transport when present), so it works for stdio too — whose base transport has no setProtocolVersion. - core: MessageTrackingTransport captures + exposes protocolVersion; InspectorClient stores it, exposes getProtocolVersion(), dispatches a protocolVersionChange event, and clears it on disconnect. - core: useInspectorClient subscribes and returns protocolVersion. - web: App reads it from the hook (drops the hard-coded "2025-06-18") and InspectorView splices it onto the active server's connection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): decouple initializeResult from protocolVersion (#1324 review) Gate the initializeResult memo on serverInfo only, not protocolVersion. initializeResult also drives the connected ViewHeader and Connection Info modal, so requiring the version risked hiding the whole connected surface in the edge case where it's absent. protocolVersion now flows through as the optional field it already is everywhere downstream ('' reads as unknown; the ServerCard label and modal value both tolerate it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web): drop redundant connected-check in InspectorView splice (#1324 review) initializeResult is derived from connectionStatus in App's memo, so a truthy initializeResult.protocolVersion already implies a live connection — the extra connectionStatus === 'connected' guard in the splice was redundant. ServerCard keeps its own status guard (dumb- component robustness). No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
121f875a0e
commit
9b558afee1
5 changed files with 273 additions and 12 deletions
|
|
@ -15,7 +15,10 @@ const httpConfig: MCPServerConfig = {
|
|||
url: "https://api.example.com/mcp",
|
||||
};
|
||||
|
||||
const connected: ConnectionState = { status: "connected" };
|
||||
const connected: ConnectionState = {
|
||||
status: "connected",
|
||||
protocolVersion: "2025-06-18",
|
||||
};
|
||||
const disconnected: ConnectionState = { status: "disconnected" };
|
||||
const connecting: ConnectionState = { status: "connecting" };
|
||||
const failed: ConnectionState = {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ const sseConfig: MCPServerConfig = {
|
|||
url: "https://api.example.com/sse",
|
||||
};
|
||||
|
||||
const connected: ConnectionState = { status: "connected" };
|
||||
const connected: ConnectionState = {
|
||||
status: "connected",
|
||||
protocolVersion: "2025-06-18",
|
||||
};
|
||||
const disconnected: ConnectionState = { status: "disconnected" };
|
||||
const connecting: ConnectionState = { status: "connecting" };
|
||||
const errored: ConnectionState = {
|
||||
|
|
@ -149,6 +152,30 @@ describe("ServerCard", () => {
|
|||
expect(container.querySelector('[aria-disabled="true"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the negotiated protocol version when connected", () => {
|
||||
renderWithMantine(<ServerCard {...baseProps} connection={connected} />);
|
||||
expect(screen.getByText("MCP 2025-06-18")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("omits the protocol version when not connected", () => {
|
||||
renderWithMantine(
|
||||
<ServerCard
|
||||
{...baseProps}
|
||||
connection={{ status: "disconnected", protocolVersion: "2025-06-18" }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText("MCP 2025-06-18")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("omits the protocol version when connected but none was negotiated", () => {
|
||||
renderWithMantine(
|
||||
<ServerCard {...baseProps} connection={{ status: "connected" }} />,
|
||||
);
|
||||
expect(
|
||||
screen.queryByText(/^MCP \d{4}-\d{2}-\d{2}$/),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("omits the version badge when info is missing", () => {
|
||||
renderWithMantine(<ServerCard {...baseProps} info={undefined} />);
|
||||
expect(screen.queryByText("1.2.0")).not.toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ const ModeText = Text.withProps({
|
|||
c: "dimmed",
|
||||
});
|
||||
|
||||
const ProtocolText = Text.withProps({
|
||||
size: "sm",
|
||||
c: "dimmed",
|
||||
ml: "auto",
|
||||
});
|
||||
|
||||
const SubtleButton = Button.withProps({
|
||||
variant: "subtle",
|
||||
size: "xs",
|
||||
|
|
@ -102,6 +108,8 @@ export function ServerCard({
|
|||
const transport = getTransport(config);
|
||||
const commandOrUrl = getCommandOrUrl(config);
|
||||
const version = info?.version;
|
||||
const protocolVersion =
|
||||
connection.status === "connected" ? connection.protocolVersion : undefined;
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
|
@ -134,6 +142,9 @@ export function ServerCard({
|
|||
{version && <Badge variant="outline">{version}</Badge>}
|
||||
<TransportBadge transport={transport} />
|
||||
<ModeText>{TRANSPORT_DESCRIPTION[transport]}</ModeText>
|
||||
{protocolVersion && (
|
||||
<ProtocolText>MCP {protocolVersion}</ProtocolText>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<ContentViewer
|
||||
|
|
|
|||
|
|
@ -198,6 +198,61 @@ describe("InspectorView", () => {
|
|||
expect(screen.getByRole("switch")).toBeChecked();
|
||||
});
|
||||
|
||||
it("surfaces the negotiated protocol version on the active connected card", () => {
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "connected",
|
||||
initializeResult: connectedInit,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
// initializeResult.protocolVersion is spliced onto the active server's
|
||||
// connection; ServerCard renders it as "MCP <version>".
|
||||
expect(screen.getByText("MCP 2025-06-18")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show a protocol version on the card while disconnected", () => {
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "disconnected",
|
||||
initializeResult: undefined,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.queryByText(/^MCP \d{4}-\d{2}-\d{2}$/),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the connected surface and hides the label when the version is unknown", () => {
|
||||
// App emits initializeResult with protocolVersion "" when the negotiated
|
||||
// version is somehow absent — the connected header/modal must still render
|
||||
// (gated on serverInfo, not the version), and the card label stays hidden.
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
servers: [sampleServer],
|
||||
activeServer: "alpha",
|
||||
connectionStatus: "connected",
|
||||
initializeResult: { ...connectedInit, protocolVersion: "" },
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
// Connected: the card toggle is on (connected surface is alive).
|
||||
expect(screen.getByRole("switch")).toBeChecked();
|
||||
// ...but no "MCP <version>" label, since the version is empty. (The
|
||||
// date-shaped matcher avoids matching the "MCP Inspector" header title.)
|
||||
expect(
|
||||
screen.queryByText(/^MCP \d{4}-\d{2}-\d{2}$/),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("snaps activeTab back to Servers when connection drops", async () => {
|
||||
const { rerender } = renderWithMantine(
|
||||
<InspectorView
|
||||
|
|
|
|||
|
|
@ -485,9 +485,23 @@ export function InspectorView({
|
|||
if (s.id !== activeServer) {
|
||||
return { ...s, connection: { status: "disconnected" } };
|
||||
}
|
||||
return { ...s, connection: { status: connectionStatus } };
|
||||
return {
|
||||
...s,
|
||||
connection: {
|
||||
status: connectionStatus,
|
||||
// Surface the negotiated protocol version on the active card.
|
||||
// initializeResult carries it (App builds it from the
|
||||
// InspectorClient handshake, #1324) and is only ever truthy when
|
||||
// connected — it's derived from connectionStatus in the same memo
|
||||
// — so its presence already implies a live connection. App uses ""
|
||||
// for an unknown version, so only set the field when it's present.
|
||||
...(initializeResult?.protocolVersion
|
||||
? { protocolVersion: initializeResult.protocolVersion }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}),
|
||||
[serversInput, activeServer, connectionStatus],
|
||||
[serversInput, activeServer, connectionStatus, initializeResult],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in a new issue