From 1da43ed310c44a83215ac9bb1e828379f8ca107d Mon Sep 17 00:00:00 2001 From: Sirius Date: Fri, 6 Feb 2026 12:32:05 +0100 Subject: [PATCH 1/6] feat(inspector): add agent-initialize event type [TASK-017-01] --- packages/inspector/src/types/inspector-event-types.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/inspector/src/types/inspector-event-types.ts b/packages/inspector/src/types/inspector-event-types.ts index be5182ea..c29f2208 100644 --- a/packages/inspector/src/types/inspector-event-types.ts +++ b/packages/inspector/src/types/inspector-event-types.ts @@ -61,7 +61,8 @@ export type InspectorEventType = | "dialog" // Agent events (session-agnostic tool calls from the inspector) | "agent-tool-call" - | "agent-tool-result"; + | "agent-tool-result" + | "agent-initialize"; /** * Inspector event record @@ -158,6 +159,7 @@ export function getEventCategory(type: InspectorEventType): EventCategory { case "agent-tool-call": case "agent-tool-result": + case "agent-initialize": return "agent"; } } @@ -270,5 +272,10 @@ export function getEventSummary(event: InspectorEvent | AgnosticInspectorEvent): const toolName = getStr(payload, "name") ?? getStr(payload, "toolName") ?? "unknown"; return isError ? `Agent Error: ${toolName}` : `Agent Result: ${toolName}`; } + + case "agent-initialize": { + const clientName = getStr(payload, "clientName") ?? getStr(payload, "name"); + return clientName ? `Agent Connected: ${clientName}` : "Agent Connected"; + } } } From 616115084d0551aa04d185a7e0fcb6a7e5b707b2 Mon Sep 17 00:00:00 2001 From: Sirius Date: Fri, 6 Feb 2026 12:34:57 +0100 Subject: [PATCH 2/6] feat(inspector): intercept MCP initialize for agent detection [TASK-017-02] --- packages/inspector/src/connection.ts | 68 +++++++++++++++++++++ packages/inspector/src/dual-server.ts | 13 ++++ packages/inspector/src/standalone-server.ts | 11 +++- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/packages/inspector/src/connection.ts b/packages/inspector/src/connection.ts index 4cd3a7df..51dffda7 100644 --- a/packages/inspector/src/connection.ts +++ b/packages/inspector/src/connection.ts @@ -1181,6 +1181,74 @@ export class ConnectionManager extends EventEmitter { return count; } + + /** + * Check if a JSON-RPC body is an MCP `initialize` request and record an agent-initialize event + * + * This intercepts the MCP initialize handshake to detect when an agent connects. + * The clientInfo.name field identifies the connecting agent (e.g., "claude-code", "cursor"). + * + * @param jsonRpcBody - Parsed JSON-RPC request body (or unknown value to check) + * @returns true if an initialize event was recorded, false otherwise + */ + maybeRecordInitialize(jsonRpcBody: unknown): boolean { + // Validate JSON-RPC structure + if (!jsonRpcBody || typeof jsonRpcBody !== "object") { + return false; + } + + const body = jsonRpcBody as Record; + + // Check if this is an initialize request + if (body.method !== "initialize") { + return false; + } + + // Extract params.clientInfo + const params = body.params; + if (!params || typeof params !== "object") { + if (this.debug) { + console.log(`[inspector] initialize request missing params`); + } + return false; + } + + const paramsObj = params as Record; + const clientInfo = paramsObj.clientInfo; + + if (!clientInfo || typeof clientInfo !== "object") { + if (this.debug) { + console.log(`[inspector] initialize request missing clientInfo`); + } + // Still record the event, just without client name + this.recordAgentEvent("agent-initialize", {}); + return true; + } + + const clientInfoObj = clientInfo as Record; + const clientName = typeof clientInfoObj.name === "string" ? clientInfoObj.name : undefined; + const clientVersion = + typeof clientInfoObj.version === "string" ? clientInfoObj.version : undefined; + + // Build payload + const payload: Record = {}; + if (clientName) { + payload.clientName = clientName; + } + if (clientVersion) { + payload.clientVersion = clientVersion; + } + + this.recordAgentEvent("agent-initialize", payload); + + if (this.debug) { + console.log( + `[inspector] Agent initialize detected: ${clientName ?? "unknown"}${clientVersion ? ` v${clientVersion}` : ""}` + ); + } + + return true; + } } // ============================================================================= diff --git a/packages/inspector/src/dual-server.ts b/packages/inspector/src/dual-server.ts index 03f469a4..49abb76b 100644 --- a/packages/inspector/src/dual-server.ts +++ b/packages/inspector/src/dual-server.ts @@ -716,6 +716,19 @@ export function createDualInspectorServer( } const body = Buffer.concat(chunks); + // Intercept MCP initialize requests on both endpoints for agent detection + if (body.length > 0 && (url.startsWith("/agent/mcp") || url.startsWith("/apps/mcp"))) { + try { + const parsed = JSON.parse(body.toString("utf-8")) as unknown; + const cm = getActiveConnectionManager(); + if (cm) { + cm.maybeRecordInitialize(parsed); + } + } catch { + // Not valid JSON, ignore + } + } + const webRequest = new Request(requestUrl, { method: req.method ?? "GET", headers: Object.entries(req.headers) diff --git a/packages/inspector/src/standalone-server.ts b/packages/inspector/src/standalone-server.ts index bb8e96be..8ce80cda 100644 --- a/packages/inspector/src/standalone-server.ts +++ b/packages/inspector/src/standalone-server.ts @@ -808,7 +808,7 @@ export function createStandaloneInspectorServer( } const body = Buffer.concat(chunks); - // Track inspector tool calls for the Agent panel + // Track inspector tool calls and agent initialization for the Agent panel let inspectorToolCall: { name: string; arguments: unknown; startTime: number } | null = null; if (body.length > 0) { try { @@ -816,6 +816,14 @@ export function createStandaloneInspectorServer( method?: string; params?: { name?: string; arguments?: unknown }; }; + + const connectionManager = getActiveConnectionManager(); + + // Check for MCP initialize request (agent detection) + if (connectionManager) { + connectionManager.maybeRecordInitialize(parsed); + } + // Check if this is a tools/call request (MCP JSON-RPC) if (parsed.method === "tools/call" && parsed.params?.name) { inspectorToolCall = { @@ -824,7 +832,6 @@ export function createStandaloneInspectorServer( startTime: Date.now(), }; // Record inspector tool call event - const connectionManager = getActiveConnectionManager(); if (connectionManager) { const eventPayload: Record = { name: inspectorToolCall.name, From d7d335f4394a939a83fca192a309c0295a1ea1b6 Mon Sep 17 00:00:00 2001 From: Sirius Date: Fri, 6 Feb 2026 12:37:27 +0100 Subject: [PATCH 3/6] feat(inspector): update NoWidgetPlaceholder with 3-state UI [TASK-017-03] --- .../react/components/NoWidgetPlaceholder.tsx | 120 +++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx b/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx index 2e2f0b53..fc70d872 100644 --- a/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx +++ b/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx @@ -1,18 +1,132 @@ /** * NoWidgetPlaceholder Component * - * Crystalline star image with floating animation. + * Stepped tutorial placeholder with 3-state UI: + * 1. No server connected - prompts to connect server + * 2. Server connected, no agent - prompts to connect agent + * 3. Agent connected - shows ready state with client name */ import React from "react"; +import type { CSSProperties } from "react"; import { styles } from "../styles"; import starUrl from "../../assets/sirius-star.png"; -export function NoWidgetPlaceholder(): React.ReactElement { +/** Connection state for the placeholder UI */ +export type ConnectionState = "no-server" | "server-connected" | "agent-connected"; + +export interface NoWidgetPlaceholderProps { + /** Current connection state */ + connectionState: ConnectionState; + /** Name of connected agent client (only set when agent-connected) */ + clientName?: string; + /** Callback when user clicks Connect Server button */ + onConnect: () => void; +} + +/** Local styles for the stepped tutorial layout */ +const localStyles: Record = { + tagline: { + color: "#6b7280", + fontSize: "0.8125rem", + textAlign: "center", + margin: 0, + letterSpacing: "0.01em", + }, + heading: { + color: "#e8e8e8", + fontSize: "1.125rem", + fontWeight: 500, + textAlign: "center", + margin: 0, + letterSpacing: "-0.01em", + }, + subtext: { + color: "#6b7280", + fontSize: "0.875rem", + textAlign: "center", + margin: 0, + lineHeight: 1.5, + }, + clientName: { + color: "#e8e8e8", + fontWeight: 600, + }, + connectButton: { + backgroundColor: "#ffffff", + color: "#000000", + border: "none", + borderRadius: "8px", + padding: "0.625rem 1.25rem", + fontSize: "0.875rem", + fontWeight: 500, + fontFamily: "inherit", + cursor: "pointer", + transition: "opacity 0.15s ease, transform 0.15s ease", + }, + stateContent: { + display: "flex", + flexDirection: "column" as const, + alignItems: "center", + gap: "0.75rem", + marginTop: "0.5rem", + }, +}; + +export function NoWidgetPlaceholder({ + connectionState, + clientName, + onConnect, +}: NoWidgetPlaceholderProps): React.ReactElement { return (
+ {/* Star logo */} Sirius the star -

No active widget yet — ask your agent to test

+ + {/* Tagline - always shown */} +

Debug MCP servers alongside your Agent

+ + {/* State-specific content */} +
+ {connectionState === "no-server" && ( + <> +

Connect the server you want to inspect

+ + + )} + + {connectionState === "server-connected" && ( + <> +

Connect your Agent to this MCP Server

+

+ The inspector will capture all tool calls and responses +

+ + )} + + {connectionState === "agent-connected" && ( + <> +

Ready to Test

+

+ with {clientName ?? "Agent"} +

+ + )} +
); } From 590df62afea7ab1331a61addebe12901c7a8ed00 Mon Sep 17 00:00:00 2001 From: Sirius Date: Fri, 6 Feb 2026 12:39:15 +0100 Subject: [PATCH 4/6] feat(inspector): hide TabBar when no connections [TASK-017-04] --- .../inspector/src/dashboard/react/components/TabBar.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/inspector/src/dashboard/react/components/TabBar.tsx b/packages/inspector/src/dashboard/react/components/TabBar.tsx index 7bd09569..30e49dcf 100644 --- a/packages/inspector/src/dashboard/react/components/TabBar.tsx +++ b/packages/inspector/src/dashboard/react/components/TabBar.tsx @@ -136,7 +136,11 @@ export function TabBar({ onSelect, onClose, onAdd, -}: TabBarProps): React.ReactElement { +}: TabBarProps): React.ReactElement | null { + if (tabs.length === 0) { + return null; + } + return (
From fe6c87a859d582c80a1240b6bbb192e86b8f8d49 Mon Sep 17 00:00:00 2001 From: Sirius Date: Fri, 6 Feb 2026 12:42:14 +0100 Subject: [PATCH 5/6] feat(inspector): wire connection state to NoWidgetPlaceholder [TASK-017-05] --- .../dashboard/react/InspectorDashboard.tsx | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/inspector/src/dashboard/react/InspectorDashboard.tsx b/packages/inspector/src/dashboard/react/InspectorDashboard.tsx index 0f1d3de0..78c97ad5 100644 --- a/packages/inspector/src/dashboard/react/InspectorDashboard.tsx +++ b/packages/inspector/src/dashboard/react/InspectorDashboard.tsx @@ -23,7 +23,7 @@ import { TabBar } from "./components/TabBar"; import { GlobalsPanel } from "./components/GlobalsPanel"; import { McpPrimitivesPanel } from "./components/McpPrimitivesPanel"; import { RightPanel } from "./components/RightPanel"; -import { NoWidgetPlaceholder } from "./components/NoWidgetPlaceholder"; +import { NoWidgetPlaceholder, type ConnectionState } from "./components/NoWidgetPlaceholder"; import { OAuthDiscoveryPanel } from "./components/OAuthDiscoveryPanel"; import { styles } from "./styles"; import logoUrl from "../assets/logo.png"; @@ -212,6 +212,32 @@ export function InspectorDashboard({ baseUrl = "" }: InspectorDashboardProps): R resources.length > 0 ? resources : (cachedState?.primitives?.resources ?? []); const displayPrompts = prompts.length > 0 ? prompts : (cachedState?.primitives?.prompts ?? []); + // Compute connection state for NoWidgetPlaceholder + const connectionState: ConnectionState = useMemo(() => { + // No server connected + if (!activeConnection || connections.length === 0) { + return "no-server"; + } + // Server connected - check for agent-initialize event + if (activeConnection.status === "connected") { + const hasAgentInit = displayAgentEvents.some((e) => e.type === "agent-initialize"); + return hasAgentInit ? "agent-connected" : "server-connected"; + } + // Connecting or error state - treat as no server + return "no-server"; + }, [activeConnection, connections.length, displayAgentEvents]); + + // Extract client name from agent-initialize event + const agentClientName = useMemo(() => { + const initEvent = displayAgentEvents.find((e) => e.type === "agent-initialize"); + return (initEvent?.payload as { clientName?: string } | undefined)?.clientName; + }, [displayAgentEvents]); + + // Handler to open connection form + const handleConnect = useCallback(() => { + setIsConnectionFormOpen(true); + }, []); + // Left panel state (MCP primitives) const [isLeftPanelCollapsed, setIsLeftPanelCollapsed] = useState(false); @@ -612,7 +638,11 @@ export function InspectorDashboard({ baseUrl = "" }: InspectorDashboardProps): R
) : ( /* Tamagotchi placeholder when no widget */ - + )} From 9c589d4b6fb234498e9d786c0045a55cc9836548 Mon Sep 17 00:00:00 2001 From: Sirius Date: Fri, 6 Feb 2026 13:02:37 +0100 Subject: [PATCH 6/6] fix(inspector): address PR review feedback [TASK-017] --- packages/inspector/src/connection.ts | 70 ++++++++-------- .../react/components/NoWidgetPlaceholder.tsx | 25 ++++-- packages/inspector/src/dual-server.ts | 3 +- packages/inspector/src/standalone-server.ts | 3 +- packages/inspector/tests/connection.test.ts | 79 +++++++++++++++++++ 5 files changed, 131 insertions(+), 49 deletions(-) diff --git a/packages/inspector/src/connection.ts b/packages/inspector/src/connection.ts index 51dffda7..9d8ace1c 100644 --- a/packages/inspector/src/connection.ts +++ b/packages/inspector/src/connection.ts @@ -5,6 +5,7 @@ */ import { EventEmitter } from "node:events"; +import { z } from "zod"; import { createTestClient, type TestClient, @@ -1182,6 +1183,24 @@ export class ConnectionManager extends EventEmitter { return count; } + /** + * Zod schema for validating MCP initialize requests. + * Used by maybeRecordInitialize to safely extract clientInfo. + */ + private static readonly InitializeRequestSchema = z.object({ + method: z.literal("initialize"), + params: z + .object({ + clientInfo: z + .object({ + name: z.string().optional(), + version: z.string().optional(), + }) + .optional(), + }) + .optional(), + }); + /** * Check if a JSON-RPC body is an MCP `initialize` request and record an agent-initialize event * @@ -1192,58 +1211,31 @@ export class ConnectionManager extends EventEmitter { * @returns true if an initialize event was recorded, false otherwise */ maybeRecordInitialize(jsonRpcBody: unknown): boolean { - // Validate JSON-RPC structure - if (!jsonRpcBody || typeof jsonRpcBody !== "object") { - return false; - } - - const body = jsonRpcBody as Record; + // Use Zod to safely validate and extract the initialize request structure + const parseResult = ConnectionManager.InitializeRequestSchema.safeParse(jsonRpcBody); - // Check if this is an initialize request - if (body.method !== "initialize") { + if (!parseResult.success) { + // Not a valid initialize request structure return false; } - // Extract params.clientInfo - const params = body.params; - if (!params || typeof params !== "object") { - if (this.debug) { - console.log(`[inspector] initialize request missing params`); - } - return false; - } - - const paramsObj = params as Record; - const clientInfo = paramsObj.clientInfo; - - if (!clientInfo || typeof clientInfo !== "object") { - if (this.debug) { - console.log(`[inspector] initialize request missing clientInfo`); - } - // Still record the event, just without client name - this.recordAgentEvent("agent-initialize", {}); - return true; - } - - const clientInfoObj = clientInfo as Record; - const clientName = typeof clientInfoObj.name === "string" ? clientInfoObj.name : undefined; - const clientVersion = - typeof clientInfoObj.version === "string" ? clientInfoObj.version : undefined; + const { params } = parseResult.data; + const clientInfo = params?.clientInfo; - // Build payload + // Build payload from validated data const payload: Record = {}; - if (clientName) { - payload.clientName = clientName; + if (clientInfo?.name) { + payload.clientName = clientInfo.name; } - if (clientVersion) { - payload.clientVersion = clientVersion; + if (clientInfo?.version) { + payload.clientVersion = clientInfo.version; } this.recordAgentEvent("agent-initialize", payload); if (this.debug) { console.log( - `[inspector] Agent initialize detected: ${clientName ?? "unknown"}${clientVersion ? ` v${clientVersion}` : ""}` + `[inspector] Agent initialize detected: ${clientInfo?.name ?? "unknown"}${clientInfo?.version ? ` v${clientInfo.version}` : ""}` ); } diff --git a/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx b/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx index fc70d872..7697ae4f 100644 --- a/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx +++ b/packages/inspector/src/dashboard/react/components/NoWidgetPlaceholder.tsx @@ -24,6 +24,20 @@ export interface NoWidgetPlaceholderProps { onConnect: () => void; } +/** + * Module-level button hover handlers. + * Extracted from inline handlers for memoization — these don't depend on component state. + */ +const handleButtonHover = (e: React.MouseEvent) => { + e.currentTarget.style.opacity = "0.9"; + e.currentTarget.style.transform = "scale(1.02)"; +}; + +const handleButtonLeave = (e: React.MouseEvent) => { + e.currentTarget.style.opacity = "1"; + e.currentTarget.style.transform = "scale(1)"; +}; + /** Local styles for the stepped tutorial layout */ const localStyles: Record = { tagline: { @@ -93,16 +107,11 @@ export function NoWidgetPlaceholder({

Connect the server you want to inspect

diff --git a/packages/inspector/src/dual-server.ts b/packages/inspector/src/dual-server.ts index 49abb76b..a67b2cbf 100644 --- a/packages/inspector/src/dual-server.ts +++ b/packages/inspector/src/dual-server.ts @@ -725,7 +725,8 @@ export function createDualInspectorServer( cm.maybeRecordInitialize(parsed); } } catch { - // Not valid JSON, ignore + // Parse failures handled silently — non-JSON bodies on MCP endpoints are expected + // (e.g., SSE connections, partial uploads). Debug logging omitted to avoid noise. } } diff --git a/packages/inspector/src/standalone-server.ts b/packages/inspector/src/standalone-server.ts index 8ce80cda..f2c3be7c 100644 --- a/packages/inspector/src/standalone-server.ts +++ b/packages/inspector/src/standalone-server.ts @@ -847,7 +847,8 @@ export function createStandaloneInspectorServer( } } } catch { - // Not valid JSON or not a tool call, ignore + // Parse failures handled silently — non-JSON bodies on MCP endpoints are expected + // (e.g., SSE connections, partial uploads). Debug logging omitted to avoid noise. } } diff --git a/packages/inspector/tests/connection.test.ts b/packages/inspector/tests/connection.test.ts index a6d66fa7..f21707ab 100644 --- a/packages/inspector/tests/connection.test.ts +++ b/packages/inspector/tests/connection.test.ts @@ -132,4 +132,83 @@ describe("ConnectionManager", () => { expect(count).toBe(0); }); }); + + describe("maybeRecordInitialize", () => { + it("should record event for valid initialize request with clientInfo", () => { + const jsonRpcBody = { + method: "initialize", + params: { + clientInfo: { + name: "claude-code", + version: "1.0.0", + }, + }, + }; + + const result = manager.maybeRecordInitialize(jsonRpcBody); + + expect(result).toBe(true); + const events = manager.getAgentEvents(); + expect(events).toHaveLength(1); + expect(events[0].type).toBe("agent-initialize"); + expect(events[0].payload).toEqual({ + clientName: "claude-code", + clientVersion: "1.0.0", + }); + }); + + it("should record event with undefined name when clientInfo is missing", () => { + const jsonRpcBody = { + method: "initialize", + params: {}, + }; + + const result = manager.maybeRecordInitialize(jsonRpcBody); + + expect(result).toBe(true); + const events = manager.getAgentEvents(); + expect(events).toHaveLength(1); + expect(events[0].type).toBe("agent-initialize"); + expect(events[0].payload).toEqual({}); + }); + + it("should return false for invalid JSON-RPC structure (no method)", () => { + const jsonRpcBody = { + params: { + clientInfo: { name: "test" }, + }, + }; + + const result = manager.maybeRecordInitialize(jsonRpcBody); + + expect(result).toBe(false); + expect(manager.getAgentEvents()).toHaveLength(0); + }); + + it("should return false for non-initialize method", () => { + const jsonRpcBody = { + method: "tools/list", + params: {}, + }; + + const result = manager.maybeRecordInitialize(jsonRpcBody); + + expect(result).toBe(false); + expect(manager.getAgentEvents()).toHaveLength(0); + }); + + it("should record event with undefined clientInfo when params is missing", () => { + const jsonRpcBody = { + method: "initialize", + }; + + const result = manager.maybeRecordInitialize(jsonRpcBody); + + expect(result).toBe(true); + const events = manager.getAgentEvents(); + expect(events).toHaveLength(1); + expect(events[0].type).toBe("agent-initialize"); + expect(events[0].payload).toEqual({}); + }); + }); });