diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx index 8b9784d92..0232780bb 100644 --- a/clients/web/src/App.tsx +++ b/clients/web/src/App.tsx @@ -220,6 +220,7 @@ function App() { capabilities, serverInfo, instructions, + protocolVersion, } = useInspectorClient(inspectorClient); const { tools, refresh: refreshTools } = useManagedTools( inspectorClient, @@ -278,18 +279,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, + ]); // Derive log entries from the message log. Filters for // `notifications/message` (the response to `logging/setLevel`). diff --git a/clients/web/src/test/core/react/useInspectorClient.test.tsx b/clients/web/src/test/core/react/useInspectorClient.test.tsx index 60e06feb0..634710e99 100644 --- a/clients/web/src/test/core/react/useInspectorClient.test.tsx +++ b/clients/web/src/test/core/react/useInspectorClient.test.tsx @@ -18,12 +18,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(); }); @@ -33,6 +35,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(); }); @@ -50,17 +53,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 () => { @@ -97,6 +102,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 08ed3028d..459454f2b 100644 --- a/clients/web/src/test/integration/mcp/inspectorClient.test.ts +++ b/clients/web/src/test/integration/mcp/inspectorClient.test.ts @@ -66,6 +66,7 @@ import type { ContentBlock, } from "@modelcontextprotocol/sdk/types.js"; import { + LATEST_PROTOCOL_VERSION, RELATED_TASK_META_KEY, McpError, ErrorCode, @@ -4196,6 +4197,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 c0ccd1859..05525a08e 100644 --- a/core/mcp/__tests__/fakeInspectorClient.ts +++ b/core/mcp/__tests__/fakeInspectorClient.ts @@ -39,6 +39,7 @@ export interface FakeInspectorClientOptions { capabilities?: ServerCapabilities; serverInfo?: Implementation; instructions?: string; + protocolVersion?: string; } export class FakeInspectorClient @@ -49,6 +50,7 @@ export class FakeInspectorClient private capabilities: ServerCapabilities | undefined; private serverInfo: Implementation | undefined; private instructions: string | undefined; + private protocolVersion: string | undefined; private appRendererClient: AppRendererClient | null = null; private sessionId: string | undefined; @@ -69,8 +71,7 @@ export class FakeInspectorClient async () => this.resourcePages.shift() ?? { resources: [] }, ); listResourceTemplates = vi.fn( - async () => - this.resourceTemplatePages.shift() ?? { resourceTemplates: [] }, + async () => this.resourceTemplatePages.shift() ?? { resourceTemplates: [] }, ); listRequestorTasks = vi.fn( async () => this.taskPages.shift() ?? { tasks: [] }, @@ -140,6 +141,7 @@ export class FakeInspectorClient this.capabilities = options.capabilities; this.serverInfo = options.serverInfo; this.instructions = options.instructions; + this.protocolVersion = options.protocolVersion; } getStatus(): ConnectionStatus { @@ -158,6 +160,10 @@ export class FakeInspectorClient return this.instructions; } + getProtocolVersion(): string | undefined { + return this.protocolVersion; + } + getAppRendererClient(): AppRendererClient | null { return this.appRendererClient; } @@ -199,6 +205,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; } @@ -215,9 +226,7 @@ export class FakeInspectorClient this.promptPages.push(...pages); } - queueResourcePages( - ...pages: Array> - ): void { + queueResourcePages(...pages: Array>): void { this.resourcePages.push(...pages); } diff --git a/core/mcp/inspectorClient.ts b/core/mcp/inspectorClient.ts index 737638481..b6797ff6c 100644 --- a/core/mcp/inspectorClient.ts +++ b/core/mcp/inspectorClient.ts @@ -147,6 +147,8 @@ export class InspectorClient extends InspectorClientEventTarget { private capabilities?: ServerCapabilities; private serverInfo?: Implementation; private instructions?: string; + private protocolVersion?: string; + private pendingInitializeRequestIds = new Set(); // Sampling requests private pendingSamples: SamplingCreateMessage[] = []; // Elicitation requests @@ -310,6 +312,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(), @@ -321,6 +326,7 @@ export class InspectorClient extends InspectorClientEventTarget { trackResponse: ( message: JSONRPCResultResponse | JSONRPCErrorResponse, ) => { + this.captureInitializeProtocolVersion(message); const entry: MessageEntry = { id: crypto.randomUUID(), timestamp: new Date(), @@ -929,10 +935,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); } /** @@ -1164,6 +1172,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 @@ -1878,6 +1893,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 85a0a5a15..80b79876a 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 612010d14..cde3f3a20 100644 --- a/core/mcp/inspectorClientProtocol.ts +++ b/core/mcp/inspectorClientProtocol.ts @@ -50,6 +50,7 @@ export interface InspectorClientProtocol extends InspectorClientEventTarget { getCapabilities(): ServerCapabilities | undefined; 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 94d56f2c2..9ab084903 100644 --- a/core/react/useInspectorClient.ts +++ b/core/react/useInspectorClient.ts @@ -13,6 +13,7 @@ export interface UseInspectorClientResult { capabilities?: ServerCapabilities; serverInfo?: Implementation; instructions?: string; + protocolVersion?: string; appRendererClient: AppRendererClient | null; connect: () => Promise; disconnect: () => Promise; @@ -26,7 +27,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. @@ -46,6 +48,9 @@ export function useInspectorClient( const [instructions, setInstructions] = useState( inspectorClient?.getInstructions(), ); + const [protocolVersion, setProtocolVersion] = useState( + inspectorClient?.getProtocolVersion(), + ); useEffect(() => { if (!inspectorClient) { @@ -53,6 +58,7 @@ export function useInspectorClient( setCapabilities(undefined); setServerInfo(undefined); setInstructions(undefined); + setProtocolVersion(undefined); return; } @@ -60,6 +66,7 @@ export function useInspectorClient( setCapabilities(inspectorClient.getCapabilities()); setServerInfo(inspectorClient.getServerInfo()); setInstructions(inspectorClient.getInstructions()); + setProtocolVersion(inspectorClient.getProtocolVersion()); const onStatusChange = (event: TypedEvent<"statusChange">) => { setStatus(event.detail); @@ -73,6 +80,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( @@ -84,6 +96,10 @@ export function useInspectorClient( "instructionsChange", onInstructionsChange, ); + inspectorClient.addEventListener( + "protocolVersionChange", + onProtocolVersionChange, + ); return () => { inspectorClient.removeEventListener("statusChange", onStatusChange); @@ -99,6 +115,10 @@ export function useInspectorClient( "instructionsChange", onInstructionsChange, ); + inspectorClient.removeEventListener( + "protocolVersionChange", + onProtocolVersionChange, + ); }; }, [inspectorClient]); @@ -117,6 +137,7 @@ export function useInspectorClient( capabilities, serverInfo, instructions, + protocolVersion, appRendererClient: inspectorClient?.getAppRendererClient() ?? null, connect, disconnect,