From 2b323f3d6015321a375e80381ab013695a3b2b48 Mon Sep 17 00:00:00 2001 From: shusingh Date: Wed, 20 May 2026 17:37:01 -0700 Subject: [PATCH] Expose negotiated protocol version in v2 --- clients/web/src/App.tsx | 19 ++++++--- .../core/react/useInspectorClient.test.tsx | 8 +++- .../integration/mcp/inspectorClient.test.ts | 15 +++++++ core/mcp/__tests__/fakeInspectorClient.ts | 12 ++++++ core/mcp/inspectorClient.ts | 39 +++++++++++++++++++ core/mcp/inspectorClientEventTarget.ts | 1 + core/mcp/inspectorClientProtocol.ts | 1 + core/react/useInspectorClient.ts | 23 ++++++++++- 8 files changed, 110 insertions(+), 8 deletions(-) diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx index 5dc00dab6..1baa4d2a1 100644 --- a/clients/web/src/App.tsx +++ b/clients/web/src/App.tsx @@ -471,6 +471,7 @@ function App() { clientCapabilities, serverInfo, instructions, + protocolVersion, } = useInspectorClient(inspectorClient); const { tools, refresh: refreshTools } = useManagedTools( inspectorClient, @@ -751,18 +752,24 @@ function App() { }, [inspectorClient]); // Build the InitializeResult the connected ViewHeader expects from the - // hook's split fields. `protocolVersion` is hard-coded for now — the - // useInspectorClient hook doesn't expose it. TODO(#1324): consume the - // negotiated value once the hook surfaces it. + // hook's split fields. const initializeResult = useMemo(() => { - if (connectionStatus !== "connected" || !serverInfo) return undefined; + if (connectionStatus !== "connected" || !serverInfo || !protocolVersion) { + return undefined; + } return { - protocolVersion: "2025-06-18", + protocolVersion, capabilities: capabilities ?? {}, serverInfo, ...(instructions ? { instructions } : {}), }; - }, [connectionStatus, capabilities, serverInfo, instructions]); + }, [ + connectionStatus, + capabilities, + serverInfo, + instructions, + protocolVersion, + ]); // 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 diff --git a/clients/web/src/test/core/react/useInspectorClient.test.tsx b/clients/web/src/test/core/react/useInspectorClient.test.tsx index c47b23e5e..8cb6f56af 100644 --- a/clients/web/src/test/core/react/useInspectorClient.test.tsx +++ b/clients/web/src/test/core/react/useInspectorClient.test.tsx @@ -19,12 +19,14 @@ describe("useInspectorClient", () => { capabilities: CAPABILITIES, serverInfo: SERVER_INFO, instructions: "hello", + protocolVersion: "2025-03-26", }); const { result } = renderHook(() => useInspectorClient(client)); expect(result.current.status).toBe("connected"); expect(result.current.capabilities).toEqual(CAPABILITIES); expect(result.current.serverInfo).toEqual(SERVER_INFO); expect(result.current.instructions).toBe("hello"); + expect(result.current.protocolVersion).toBe("2025-03-26"); expect(result.current.appRendererClient).toBeNull(); }); @@ -34,6 +36,7 @@ describe("useInspectorClient", () => { expect(result.current.capabilities).toBeUndefined(); expect(result.current.serverInfo).toBeUndefined(); expect(result.current.instructions).toBeUndefined(); + expect(result.current.protocolVersion).toBeUndefined(); expect(result.current.appRendererClient).toBeNull(); }); @@ -51,17 +54,19 @@ describe("useInspectorClient", () => { expect(result.current.status).toBe("connected"); }); - it("subscribes to capabilities/serverInfo/instructions changes", () => { + it("subscribes to capabilities/serverInfo/instructions/protocolVersion changes", () => { const client = new FakeInspectorClient(); const { result } = renderHook(() => useInspectorClient(client)); act(() => { client.setCapabilities(CAPABILITIES); client.setServerInfo(SERVER_INFO); client.setInstructions("after"); + client.setProtocolVersion("2025-06-18"); }); expect(result.current.capabilities).toEqual(CAPABILITIES); expect(result.current.serverInfo).toEqual(SERVER_INFO); expect(result.current.instructions).toBe("after"); + expect(result.current.protocolVersion).toBe("2025-06-18"); }); it("connect() and disconnect() proxy to the client and update status", async () => { @@ -98,6 +103,7 @@ describe("useInspectorClient", () => { rerender({ c: null }); expect(result.current.status).toBe("disconnected"); expect(result.current.capabilities).toBeUndefined(); + expect(result.current.protocolVersion).toBeUndefined(); }); it("re-subscribes when the client prop changes", () => { diff --git a/clients/web/src/test/integration/mcp/inspectorClient.test.ts b/clients/web/src/test/integration/mcp/inspectorClient.test.ts index 5b5af9d52..3968fad41 100644 --- a/clients/web/src/test/integration/mcp/inspectorClient.test.ts +++ b/clients/web/src/test/integration/mcp/inspectorClient.test.ts @@ -67,6 +67,7 @@ import type { ContentBlock, } from "@modelcontextprotocol/sdk/types.js"; import { + LATEST_PROTOCOL_VERSION, RELATED_TASK_META_KEY, McpError, ErrorCode, @@ -4560,6 +4561,20 @@ describe("InspectorClient", () => { }); describe("capability detection after connect", () => { + it("captures the negotiated protocol version from initialize", async () => { + server = createTestServerHttp({ + serverInfo: createTestServerInfo(), + }); + await server.start(); + client = new InspectorClient( + { type: "streamable-http", url: server.url }, + { environment: { transport: createTransportNode } }, + ); + await client.connect(); + + expect(client.getProtocolVersion()).toBe(LATEST_PROTOCOL_VERSION); + }); + it("round-trips listChanged + subscribe flags via getCapabilities()", async () => { // The handler-registration arrows in InspectorClient fire during // connect only when the matching server capability is advertised. diff --git a/core/mcp/__tests__/fakeInspectorClient.ts b/core/mcp/__tests__/fakeInspectorClient.ts index 230eda4b1..e2f007740 100644 --- a/core/mcp/__tests__/fakeInspectorClient.ts +++ b/core/mcp/__tests__/fakeInspectorClient.ts @@ -43,6 +43,7 @@ export interface FakeInspectorClientOptions { clientCapabilities?: ClientCapabilities; serverInfo?: Implementation; instructions?: string; + protocolVersion?: string; } export class FakeInspectorClient @@ -54,6 +55,7 @@ export class FakeInspectorClient private clientCapabilities: ClientCapabilities; private serverInfo: Implementation | undefined; private instructions: string | undefined; + private protocolVersion: string | undefined; private appRendererClient: AppRendererClient | null = null; private sessionId: string | undefined; private pendingSamples: SamplingCreateMessage[] = []; @@ -147,6 +149,7 @@ export class FakeInspectorClient this.clientCapabilities = options.clientCapabilities ?? {}; this.serverInfo = options.serverInfo; this.instructions = options.instructions; + this.protocolVersion = options.protocolVersion; } getStatus(): ConnectionStatus { @@ -169,6 +172,10 @@ export class FakeInspectorClient return this.instructions; } + getProtocolVersion(): string | undefined { + return this.protocolVersion; + } + getAppRendererClient(): AppRendererClient | null { return this.appRendererClient; } @@ -235,6 +242,11 @@ export class FakeInspectorClient this.dispatchTypedEvent("instructionsChange", instructions); } + setProtocolVersion(protocolVersion: string | undefined): void { + this.protocolVersion = protocolVersion; + this.dispatchTypedEvent("protocolVersionChange", protocolVersion); + } + setAppRendererClient(client: AppRendererClient | null): void { this.appRendererClient = client; } diff --git a/core/mcp/inspectorClient.ts b/core/mcp/inspectorClient.ts index b9125b050..eb1276b6d 100644 --- a/core/mcp/inspectorClient.ts +++ b/core/mcp/inspectorClient.ts @@ -160,6 +160,8 @@ export class InspectorClient extends InspectorClientEventTarget { // UI surfaces (Server Info modal) can display them without poking at the // SDK Client's private state. private clientCapabilities: ClientCapabilities = {}; + private protocolVersion?: string; + private pendingInitializeRequestIds = new Set(); // Sampling requests private pendingSamples: SamplingCreateMessage[] = []; // Elicitation requests @@ -338,6 +340,9 @@ export class InspectorClient extends InspectorClientEventTarget { private createMessageTrackingCallbacks(): MessageTrackingCallbacks { return { trackRequest: (message: JSONRPCRequest) => { + if (message.method === "initialize") { + this.pendingInitializeRequestIds.add(message.id); + } const entry: MessageEntry = { id: crypto.randomUUID(), timestamp: new Date(), @@ -349,6 +354,7 @@ export class InspectorClient extends InspectorClientEventTarget { trackResponse: ( message: JSONRPCResultResponse | JSONRPCErrorResponse, ) => { + this.captureInitializeProtocolVersion(message); const entry: MessageEntry = { id: crypto.randomUUID(), timestamp: new Date(), @@ -1021,10 +1027,12 @@ export class InspectorClient extends InspectorClientEventTarget { this.capabilities = undefined; this.serverInfo = undefined; this.instructions = undefined; + this.protocolVersion = undefined; this.dispatchTypedEvent("pendingSamplesChange", this.pendingSamples); this.dispatchTypedEvent("capabilitiesChange", this.capabilities); this.dispatchTypedEvent("serverInfoChange", this.serverInfo); this.dispatchTypedEvent("instructionsChange", this.instructions); + this.dispatchTypedEvent("protocolVersionChange", this.protocolVersion); } /** @@ -1265,6 +1273,13 @@ export class InspectorClient extends InspectorClientEventTarget { return this.instructions; } + /** + * Get the negotiated MCP protocol version from the initialize response + */ + getProtocolVersion(): string | undefined { + return this.protocolVersion; + } + /** * Set the logging level for the MCP server * @param level Logging level to set @@ -2016,6 +2031,30 @@ export class InspectorClient extends InspectorClientEventTarget { } } + private captureInitializeProtocolVersion( + message: JSONRPCResultResponse | JSONRPCErrorResponse, + ): void { + if (message.id === undefined) { + return; + } + if (!this.pendingInitializeRequestIds.delete(message.id)) { + return; + } + if (!("result" in message)) { + return; + } + const result = message.result; + if ( + typeof result === "object" && + result !== null && + "protocolVersion" in result && + typeof result.protocolVersion === "string" + ) { + this.protocolVersion = result.protocolVersion; + this.dispatchTypedEvent("protocolVersionChange", this.protocolVersion); + } + } + private dispatchStderrLog(entry: StderrLogEntry): void { this.dispatchTypedEvent("stderrLog", entry); } diff --git a/core/mcp/inspectorClientEventTarget.ts b/core/mcp/inspectorClientEventTarget.ts index c09754d84..fd5d01471 100644 --- a/core/mcp/inspectorClientEventTarget.ts +++ b/core/mcp/inspectorClientEventTarget.ts @@ -55,6 +55,7 @@ export interface InspectorClientEventMap { capabilitiesChange: ServerCapabilities | undefined; serverInfoChange: Implementation | undefined; instructionsChange: string | undefined; + protocolVersionChange: string | undefined; message: MessageEntry; stderrLog: StderrLogEntry; fetchRequest: FetchRequestEntry; diff --git a/core/mcp/inspectorClientProtocol.ts b/core/mcp/inspectorClientProtocol.ts index 2b0ddaabf..64102ad0d 100644 --- a/core/mcp/inspectorClientProtocol.ts +++ b/core/mcp/inspectorClientProtocol.ts @@ -54,6 +54,7 @@ export interface InspectorClientProtocol extends InspectorClientEventTarget { getClientCapabilities(): ClientCapabilities; getServerInfo(): Implementation | undefined; getInstructions(): string | undefined; + getProtocolVersion(): string | undefined; getAppRendererClient(): AppRendererClient | null; // Connection control diff --git a/core/react/useInspectorClient.ts b/core/react/useInspectorClient.ts index e5f211b01..04fe4dcc6 100644 --- a/core/react/useInspectorClient.ts +++ b/core/react/useInspectorClient.ts @@ -21,6 +21,7 @@ export interface UseInspectorClientResult { clientCapabilities: ClientCapabilities; serverInfo?: Implementation; instructions?: string; + protocolVersion?: string; appRendererClient: AppRendererClient | null; connect: () => Promise; disconnect: () => Promise; @@ -34,7 +35,8 @@ export interface UseInspectorClientResult { * Note: `appRendererClient` is read lazily from the client on every render * and is NOT subscribed. It changes once at connect time and is not expected * to change again during a session, so callers will see the current value - * on any rerender triggered by status / capabilities / serverInfo / instructions. + * on any rerender triggered by status / capabilities / serverInfo / + * instructions / protocolVersion. * If a future use case requires autonomous updates when the renderer attaches, * add an `appRendererClientChange` event to `InspectorClientEventMap` and * subscribe here. @@ -54,6 +56,9 @@ export function useInspectorClient( const [instructions, setInstructions] = useState( inspectorClient?.getInstructions(), ); + const [protocolVersion, setProtocolVersion] = useState( + inspectorClient?.getProtocolVersion(), + ); useEffect(() => { if (!inspectorClient) { @@ -61,6 +66,7 @@ export function useInspectorClient( setCapabilities(undefined); setServerInfo(undefined); setInstructions(undefined); + setProtocolVersion(undefined); return; } @@ -68,6 +74,7 @@ export function useInspectorClient( setCapabilities(inspectorClient.getCapabilities()); setServerInfo(inspectorClient.getServerInfo()); setInstructions(inspectorClient.getInstructions()); + setProtocolVersion(inspectorClient.getProtocolVersion()); const onStatusChange = (event: TypedEvent<"statusChange">) => { setStatus(event.detail); @@ -81,6 +88,11 @@ export function useInspectorClient( const onInstructionsChange = (event: TypedEvent<"instructionsChange">) => { setInstructions(event.detail); }; + const onProtocolVersionChange = ( + event: TypedEvent<"protocolVersionChange">, + ) => { + setProtocolVersion(event.detail); + }; inspectorClient.addEventListener("statusChange", onStatusChange); inspectorClient.addEventListener( @@ -92,6 +104,10 @@ export function useInspectorClient( "instructionsChange", onInstructionsChange, ); + inspectorClient.addEventListener( + "protocolVersionChange", + onProtocolVersionChange, + ); return () => { inspectorClient.removeEventListener("statusChange", onStatusChange); @@ -107,6 +123,10 @@ export function useInspectorClient( "instructionsChange", onInstructionsChange, ); + inspectorClient.removeEventListener( + "protocolVersionChange", + onProtocolVersionChange, + ); }; }, [inspectorClient]); @@ -132,6 +152,7 @@ export function useInspectorClient( inspectorClient?.getClientCapabilities() ?? EMPTY_CLIENT_CAPABILITIES, serverInfo, instructions, + protocolVersion, appRendererClient: inspectorClient?.getAppRendererClient() ?? null, connect, disconnect,