Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions clients/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ function App() {
capabilities,
serverInfo,
instructions,
protocolVersion,
} = useInspectorClient(inspectorClient);
const { tools, refresh: refreshTools } = useManagedTools(
inspectorClient,
Expand Down Expand Up @@ -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<InitializeResult | undefined>(() => {
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`).
Expand Down
8 changes: 7 additions & 1 deletion clients/web/src/test/core/react/useInspectorClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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 () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
15 changes: 15 additions & 0 deletions clients/web/src/test/integration/mcp/inspectorClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import type {
ContentBlock,
} from "@modelcontextprotocol/sdk/types.js";
import {
LATEST_PROTOCOL_VERSION,
RELATED_TASK_META_KEY,
McpError,
ErrorCode,
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 14 additions & 5 deletions core/mcp/__tests__/fakeInspectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface FakeInspectorClientOptions {
capabilities?: ServerCapabilities;
serverInfo?: Implementation;
instructions?: string;
protocolVersion?: string;
}

export class FakeInspectorClient
Expand All @@ -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;

Expand All @@ -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: [] },
Expand Down Expand Up @@ -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 {
Expand All @@ -158,6 +160,10 @@ export class FakeInspectorClient
return this.instructions;
}

getProtocolVersion(): string | undefined {
return this.protocolVersion;
}

getAppRendererClient(): AppRendererClient | null {
return this.appRendererClient;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -215,9 +226,7 @@ export class FakeInspectorClient
this.promptPages.push(...pages);
}

queueResourcePages(
...pages: Array<ListResult<"resources", Resource>>
): void {
queueResourcePages(...pages: Array<ListResult<"resources", Resource>>): void {
this.resourcePages.push(...pages);
}

Expand Down
39 changes: 39 additions & 0 deletions core/mcp/inspectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSONRPCRequest["id"]>();
// Sampling requests
private pendingSamples: SamplingCreateMessage[] = [];
// Elicitation requests
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions core/mcp/inspectorClientEventTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions core/mcp/inspectorClientProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion core/react/useInspectorClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface UseInspectorClientResult {
capabilities?: ServerCapabilities;
serverInfo?: Implementation;
instructions?: string;
protocolVersion?: string;
appRendererClient: AppRendererClient | null;
connect: () => Promise<void>;
disconnect: () => Promise<void>;
Expand All @@ -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.
Expand All @@ -46,20 +48,25 @@ export function useInspectorClient(
const [instructions, setInstructions] = useState<string | undefined>(
inspectorClient?.getInstructions(),
);
const [protocolVersion, setProtocolVersion] = useState<string | undefined>(
inspectorClient?.getProtocolVersion(),
);

useEffect(() => {
if (!inspectorClient) {
setStatus("disconnected");
setCapabilities(undefined);
setServerInfo(undefined);
setInstructions(undefined);
setProtocolVersion(undefined);
return;
}

setStatus(inspectorClient.getStatus());
setCapabilities(inspectorClient.getCapabilities());
setServerInfo(inspectorClient.getServerInfo());
setInstructions(inspectorClient.getInstructions());
setProtocolVersion(inspectorClient.getProtocolVersion());

const onStatusChange = (event: TypedEvent<"statusChange">) => {
setStatus(event.detail);
Expand All @@ -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(
Expand All @@ -84,6 +96,10 @@ export function useInspectorClient(
"instructionsChange",
onInstructionsChange,
);
inspectorClient.addEventListener(
"protocolVersionChange",
onProtocolVersionChange,
);

return () => {
inspectorClient.removeEventListener("statusChange", onStatusChange);
Expand All @@ -99,6 +115,10 @@ export function useInspectorClient(
"instructionsChange",
onInstructionsChange,
);
inspectorClient.removeEventListener(
"protocolVersionChange",
onProtocolVersionChange,
);
};
}, [inspectorClient]);

Expand All @@ -117,6 +137,7 @@ export function useInspectorClient(
capabilities,
serverInfo,
instructions,
protocolVersion,
appRendererClient: inspectorClient?.getAppRendererClient() ?? null,
connect,
disconnect,
Expand Down