Inspector V2 UX - Component Interfaces Implementation Plan

July 26, 2026 · View on GitHub

Brief | V1 Problems | V2 Scope | V2 Tech Stack | V2 UX | V2 Auth | V2 New Spec Impact

Overview | Features | Handlers | Screenshots | Components | Interfaces | Plan

Goal

Refactor every component under clients/web/src/components/ so its prop interface aligns with the V2 UX Component Interfaces spec. After this work, every dumb component consumes either:

  1. A raw MCP schema type imported from @modelcontextprotocol/sdk/types.js (Tool, Prompt, Resource, ServerCapabilities, Implementation, LoggingMessageNotification, Task, etc.), or
  2. An Inspector-owned wrapper type for a UI concept that has no MCP equivalent (MCPServerConfig, ConnectionStatus, MessageEntry, StderrLogEntry, FetchRequestEntry, …), or
  3. A callback with a signature shaped by the MCP request/result types the wiring layer will eventually pass through, or
  4. Local UI state (search text, compact toggle, selected name, etc.) that is screen-owned and not derived from any MCP object.

Sources of truth

  • MCP schema types@modelcontextprotocol/sdk (TypeScript SDK), pinned at the latest 1.x line (currently ^1.29.0). The SDK exports every type this plan references, including Task and LoggingLevel.

  • Inspector-owned wrapper types — the v1.5/main branch of this repo is the canonical reference. v1.5 already has a complete core/mcp/types.ts (416 lines) defining every wrapper type the v2 components need:

    • core/mcp/types.tsMCPServerConfig discriminated union (StdioServerConfig/SseServerConfig/StreamableHttpServerConfig), ServerType, MCPConfig, ConnectionStatus, StderrLogEntry, MessageEntry, FetchRequestEntry, FetchRequestEntryBase, ServerState, ResourceReadInvocation, ResourceTemplateReadInvocation, PromptGetInvocation, ToolCallInvocation, InspectorClientEnvironment, InspectorClientOptions, CreateTransport, AppRendererClient.
    • core/mcp/taskNotificationSchemas.ts — Inspector's notifications/tasks/list_changed zod extension (the SDK exports Task and TaskStatusNotificationSchema natively, but not the list-changed signal).
    • core/react/useInspectorClient.tsUseInspectorClientResult shape (status, capabilities, serverInfo, instructions, appRendererClient, connect, disconnect).
    • core/react/useManaged{Tools,Prompts,Resources,ResourceTemplates,RequestorTasks}.ts — confirmed return shapes the screen layer will eventually consume: Tool[], Prompt[], Resource[], ResourceTemplate[], Task[].
    • core/react/{useMessageLog,useStderrLog,useFetchRequestLog}.ts — log wrapper consumers.
    • clients/web/src/utils/customHeaders.tsCustomHeader / CustomHeaders shape, used by ServerSettingsForm.
    • clients/web/src/utils/schemaUtils.ts + clients/web/src/utils/jsonUtils.ts — JSON Schema typing (JsonValue, JsonSchemaType, JsonObject) and AJV-backed validation for tool input/output schemas.

    Rule: when a wrapper type is needed in v2, copy the v1.5 definition verbatim (including field names, optionality, and Date vs string choices). Do not invent new names. The interfaces doc's Inspector* prefixes (e.g. InspectorHistoryEntry, InspectorTask, InspectorServerConfig, InspectorTaskStatus) are placeholders — they resolve to v1.5's actual names below.

Name-mapping correction (interfaces doc → v1.5 source of truth)

Several names invented in v2_ux_interfaces.md are already defined in v1.5 and should be replaced verbatim during the refactor:

v2_ux_interfaces.md placeholderv1.5 actualSource
InspectorServerConfigMCPServerConfig (discriminated union)core/mcp/types.ts
InspectorTransportTypeServerType ("stdio" | "sse" | "streamable-http")core/mcp/types.ts
ConnectionStatusConnectionStatus (already named that — "disconnected" | "connecting" | "connected" | "error")core/mcp/types.ts
InspectorHistoryEntryMessageEntry (note timestamp: Date, not string)core/mcp/types.ts
InspectorTaskTask (raw SDK type — exported by MCP SDK 1.x at @modelcontextprotocol/sdk/types.js)SDK
InspectorTaskStatusStatus field on the SDK Task type — no separate wrapper neededSDK
InspectorLogEntry(consume LoggingMessageNotification['params'] directly, plus a v2-side { id, receivedAt } lift)SDK + light wrapper
InspectorPendingRequestNo first-class wrapper in v1.5 — pending sampling/elicitation flows are coordinated as raw SDK request handlers inside InspectorClient with UI state living locally. v2 may need a small { id, request } wrapper if the dumb component needs an id key — keep it minimal.n/a
InspectorResourceSubscriptionNo dedicated wrapper in v1.5; v2 should add { resource: Resource; lastUpdated?: Date } per the interfaces spec, placed in core/mcp/types.ts alongside the others.new (v2-only)
InspectorOAuthDetailsInspectorClientOptions['oauth'] shape from core/mcp/types.ts (clientId, clientSecret, clientMetadataUrl, scope)core/mcp/types.ts
InspectorServerJsonDraftNo v1.5 equivalent — v2-only. Keep the wrapper but place it in core/mcp/types.ts next to MCPConfig.new (v2-only)
InspectorServerSettingsClosest v1.5 analog is InspectorClientOptions (timeouts, OAuth, sampling/elicit/roots flags). Settings form should accept the relevant subset.core/mcp/types.ts
InspectorRequestHistoryItemSubset of MessageEntry filtered to outbound requests. No new type needed.core/mcp/types.ts
InspectorUrlElicitRequestURL-mode elicitation IS supported by v1.5 via InspectorClientOptions.elicit: { url: true }; the request/response shape lives in core/mcp/elicitationCreateMessage.ts. v2 should mirror that file's types rather than invent.core/mcp/elicitationCreateMessage.ts
InspectorTabNo v1.5 analog — v2-only enum. Superseded by InspectorTabId in clients/web/src/utils/inspectorTabs.ts (#1785).new (v2-only)

Non-goals

  • Building the v2 core/ hook layer. This plan only refactors component prop interfaces and their Storybook fixtures. The hooks (useTools, usePrompts, useConnection, etc.) that will eventually populate those props are out of scope here. They'll arrive in a separate effort, and the contracts produced by this work will be the input spec for that effort.
  • Wiring App.tsx to the MCP transport. The current App.tsx is a 42-line theme-toggle shell. It stays that way until the core hook layer exists.
  • Updating CLI / TUI clients. Those don't exist yet in this repo.
  • Changing the visual design of any component. Refactors are interface-only; Storybook output should look identical (same screenshots) unless a new field becomes visible because it was previously dropped (e.g. Annotations on ResourceListItem).
  • Adding tests beyond what Storybook gives us. Vitest unit tests for the refactored components are a follow-up — the AGENTS.md 90% coverage rule will be addressed in its own pass once the interface dust settles.

Current state (snapshot taken on this branch)

  • clients/web/ is the only client. No core/, no cli/, no tui/, no shared clients/launcher/.
  • clients/web/src/App.tsx is a 42-line MantineProvider shell — no wiring.
  • clients/web/package.json has no @modelcontextprotocol/sdk dependency.
  • Components live under clients/web/src/components/{elements,groups,screens,views}, each in its own folder with <Name>.tsx + <Name>.stories.tsx.
  • Component count: 16 elements, 37 groups, 7 screens, 2 views = 62 components.
  • Components use ad hoc local types and flat scalar props (name: string, version: string, transport: "stdio" | "http", level: LogLevel, …) invented during the visual-design phase. Several local re-declarations exist for what should be schema types: JsonSchema, LogLevel, PromptItem, ResourceItem, ToolListItemProps, TaskStatus, RootEntry, etc.

Phase 0 — Foundations

Before any component is touched, decide and stand up the things every later phase depends on.

0.1 Add the MCP SDK as a dependency

  • Add @modelcontextprotocol/sdk to clients/web/package.json so component files can import type { Tool, Prompt, Resource, Task, LoggingLevel, ... } from "@modelcontextprotocol/sdk/types.js";.
  • Pin to ^1.29.0 (latest 1.x line as of writing). v1.5 is on ^1.25.2, but the 1.29 schema bundle is a strict superset and includes the latest task / progress refinements.
  • Verify TypeScript path resolution by importing one type into a throwaway test file and running npm run build.
  • If 1.29 introduces a breaking change vs 1.25 that affects a wrapper type copied from v1.5, prefer adapting the v2 wrapper to 1.29 — do not roll the SDK back.

0.2 Stand up the Inspector wrapper-types module

The interfaces doc invents Inspector* wrapper names; the v1.5 branch of this repo has the canonical definitions under different (cleaner) names. Use v1.5 as the source of truth — see the Sources of truth and Name-mapping correction sections above.

Decision: create core/mcp/types.ts in v2 at the same path v1.5 uses, copying the types-only subset of v1.5's core/mcp/types.ts verbatim. This sets up the directory structure for the eventual v2 core hook layer (which is out of scope for this plan but will arrive in a follow-up effort) and avoids two consecutive renames.

Subset to copy from v1.5 core/mcp/types.ts (types only — no runtime helpers, no InspectorClient constructor types):

  • StdioServerConfig, SseServerConfig, StreamableHttpServerConfig, MCPServerConfig, ServerType, MCPConfig
  • ConnectionStatus
  • StderrLogEntry
  • MessageEntry (note: timestamp: Date, not string)
  • FetchRequestEntry, FetchRequestEntryBase, FetchRequestCategory
  • ServerState
  • ResourceReadInvocation, ResourceTemplateReadInvocation, PromptGetInvocation, ToolCallInvocation

Skip from v1.5 (transport / OAuth / lifecycle types — these belong in the eventual v2 core hook layer, not this plan):

  • CreateTransport, CreateTransportOptions, CreateTransportResult
  • InspectorClientEnvironment, InspectorClientOptions, AppRendererClient

Add new in v2 (v2-only wrappers needed by the interfaces refactor that have no v1.5 equivalent):

  • InspectorResourceSubscription = { resource: Resource; lastUpdated?: Date }
  • InspectorServerJsonDraft = { rawText: string; parsed?: RegistryServerJson; selectedPackageIndex?: number; envOverrides: Record<string, string>; nameOverride?: string }
  • InspectorTab enum ("tools" | "prompts" | "resources" | "logs" | "tasks" | "history")

Tab enum placement: InspectorTab is a UI-routing concept, not an MCP or transport concept. Place it under clients/web/src/types/navigation.ts rather than in core/mcp/types.ts. Everything else lives in core/mcp/types.ts. (Superseded by #1785: the live tab-id type is InspectorTabId in clients/web/src/utils/inspectorTabs.ts, and the dead navigation.ts InspectorTab was removed — a pure UI domain type belongs in utils/, not src/types/, per the src/lib vs src/utils rule in AGENTS.md.)

Custom headers: clients/web/src/utils/customHeaders.ts is a verbatim copy of the v1.5 module. It owns CustomHeader / CustomHeaders shape used by ServerSettingsForm and the experimental panel.

JSON Schema typing for SchemaForm: copy v1.5's clients/web/src/utils/jsonUtils.ts (defines JsonValue, JsonObject, JsonSchemaType) and clients/web/src/utils/schemaUtils.ts (AJV-backed validators). This resolves the JSON Schema typing risk and keeps SchemaForm aligned with how v1.5 validates Tool.inputSchema and Tool.outputSchema.

Elicitation URL request shape: copy the URL-mode elicitation request/result types from v1.5 core/mcp/elicitationCreateMessage.ts into the new core/mcp/types.ts (or a peer file). The interfaces doc's InspectorUrlElicitRequest placeholder maps to whatever v1.5 calls them.

0.3 Vet every "likely" hedge in v2_ux_interfaces.md

v2_ux_interfaces.md was produced by parallel research subagents that I instructed to prefix uncertain hook names with (likely) rather than fail to fill in a section. The result: 52 instances of "likely" survived to the doc, almost all of them attached to hook names or hook-name-to-callback mappings that were never cross-checked against v1.5. Several are actively wrong. Before any component is touched, walk through every likely and either resolve it against v1.5 / the SDK or strike it.

Use these source-of-truth lookups, not guessing:

  • v1.5 React hooksgit show origin/v1.5/main:core/react/<hook>.ts. The complete v1.5 hook surface is:
    • useInspectorClient.tsstatus, capabilities, serverInfo, instructions, appRendererClient, connect, disconnect.
    • useManagedTools.ts{ tools: Tool[]; refresh }
    • useManagedPrompts.ts{ prompts: Prompt[]; refresh }
    • useManagedResources.ts{ resources: Resource[]; refresh }
    • useManagedResourceTemplates.ts{ resourceTemplates: ResourceTemplate[]; refresh }
    • useManagedRequestorTasks.ts{ tasks: Task[]; refresh }
    • useMessageLog.ts — JSON-RPC message buffer (MessageEntry[])
    • useStderrLog.ts — stdio stderr buffer (StderrLogEntry[])
    • useFetchRequestLog.ts — auth/transport HTTP fetch buffer (FetchRequestEntry[])
    • usePagedTools.ts, usePagedPrompts.ts, usePagedResources.ts, usePagedResourceTemplates.ts, usePagedRequestorTasks.ts — paged siblings of the managed hooks above.
  • MCP SDK typesimport type { ... } from "@modelcontextprotocol/sdk/types.js".
  • v1.5 core typesgit show origin/v1.5/main:core/mcp/types.ts.

Confirmed hook-name corrections

v2_ux_interfaces.md (likely) claimActual v1.5 surface
useToolsuseManagedTools (core/react/useManagedTools.ts)
usePromptsuseManagedPrompts
useResourcesuseManagedResources
useResourceTemplates (implied)useManagedResourceTemplates
useTasksuseManagedRequestorTasks
useMcpClientuseInspectorClient
useServerCapabilitiesNot a separate hook — capabilities: ServerCapabilities is a field on useInspectorClient's result. Strike.
useLogsThree v1.5 hooks: useMessageLog, useStderrLog, useFetchRequestLog. The interfaces doc conflates them. Split per use site: log stream → useMessageLog; stdio stderr → useStderrLog; HTTP fetch (auth/transport) → useFetchRequestLog.
useHistoryuseMessageLog — v1.5's message log IS the history; MessageEntry is the wrapper type. Strike useHistory.
useElicitationNo discrete hook in v1.5. Elicitation is handled inside InspectorClient (core/mcp/elicitationCreateMessage.ts) via SDK request handlers. UI state for pending elicitations lives wherever the consuming screen puts it. Strike useElicitation and replace with "handled inside InspectorClient request handlers; v2 will need a small UI-state hook for the pending queue but it doesn't exist yet".
useSamplingNo discrete hook in v1.5. Same shape as elicitation: core/mcp/samplingCreateMessage.ts plus SDK request handlers. Strike.
useRootsNo discrete hook in v1.5. Roots are configured via InspectorClientOptions.roots: Root[] at construction time and answered by an SDK request handler inside InspectorClient. There is no React hook. Strike or mark as "v2 to introduce".
useServersNot in v1.5. v2-only — keep, but mark explicitly as "v2 to introduce, no v1.5 analog".
useServerRegistryNot in v1.5. v2-only — same treatment.
useClientCapabilitiesNot in v1.5. v2-only — same treatment.
useInspectorNavigationNot in v1.5. Pure speculation in the interfaces doc. v2 may not need a hook at all (tab state can be useState in the wiring layer). Strike or mark "may not exist".

Confirmed wrapper / type corrections

These are also (likely)-flavored claims that slipped past the spec:

  • The interfaces doc says "MCP 2025-11-25 base schema does not define a first-class task primitive". This is wrong — the MCP TS SDK 1.x exports Task from @modelcontextprotocol/sdk/types.js, and v1.5's useManagedRequestorTasks already returns Task[]. Update every entry in the interfaces doc that proposes an InspectorTask wrapper to use Task directly. The list-changed signal is an Inspector extension (no SDK schema for notifications/tasks/list_changed); v1.5 defines it in core/mcp/taskNotificationSchemas.ts. Cite that file rather than inventing.
  • Appendix A line 854 asserts: "v1.5 monorepo bundles most primitive-specific logic inside client/src/App.tsx; hooks marked 'likely' will be extracted cleanly in v2 core". This is wrong about v1.5/main of THIS repo. It's true of the upstream modelcontextprotocol/inspector v1.x mainline, which an earlier subagent conflated with v1.5. v1.5 in this repo already has the full core/react/* hook split-out. Rewrite Appendix A to cite the actual v1.5 file paths under core/react/ and remove the "will be extracted cleanly" framing.
  • Appendix A's pointer to client/src/lib/hooks in v1.5 is also wrong-source: that path is the upstream repo, not this repo. The path in this repo's v1.5 branch is core/react/. Update the link.
  • Anywhere the doc references a LogEntry shape that mixes in a string timestamp: v1.5's MessageEntry and StderrLogEntry use Date, not string. Reconcile.

Procedure

  1. Open v2_ux_interfaces.md and walk every likely instance in document order (grep -n "likely" specification/v2_ux_interfaces.md).
  2. For each, look up the claim against v1.5 (git show origin/v1.5/main:...) or the SDK (grep in node_modules/@modelcontextprotocol/sdk/dist/types.d.ts once 0.1 lands).
  3. Either replace likely useFoo with the verified name or, if there is no v1.5 analog, replace it with an explicit v2 to introduce — no v1.5 analog annotation. Do not leave (likely) in the doc.
  4. While doing this, fix Appendix A's structural errors above.
  5. Land the corrected v2_ux_interfaces.md as the second commit of Phase 0, immediately after the core/mcp/types.ts setup commit. The commit message should call out the rename table from this section so reviewers can audit it.
  6. After 0.3 ships, grep "likely" specification/v2_ux_interfaces.md must return zero results. This is the definition-of-done for Phase 0.3.

(See Risks #11 below for the underlying root cause and the "vet weasel words" rule that should now apply to every planning doc in this repo.)

0.4 Inventory current local re-declarations to delete

Run a single pass to find every local type that will be replaced by an MCP schema type or a wrapper. Add a checklist to the PR description so reviewers can confirm all of them are gone by the end:

  • JsonSchema (in groups/SchemaForm/SchemaForm.tsx) → JsonSchemaType from the new clients/web/src/utils/jsonUtils.ts (copied from v1.5).
  • LogLevel (in elements/LogEntry/LogEntry.tsx) → LoggingLevel from the SDK.
  • TaskStatus (in groups/TaskCard/TaskCard.tsx) → status field on the SDK Task type. Do not introduce a separate InspectorTaskStatus.
  • TaskCardProps-as-data-shape (in groups/TaskListPanel/TaskListPanel.tsx) → SDK Task[].
  • PromptItem, SelectedPromptPrompt.
  • ResourceItem, TemplateListItem, SubscriptionItemResource, ResourceTemplate, InspectorResourceSubscription.
  • ToolListItemProps-as-data-shape → Tool.
  • RootEntryRoot.
  • KeyValuePair (in ServerSettingsForm) → keep, but reconcile with CustomHeader from v1.5; if the form is editing custom headers specifically, prefer CustomHeader.
  • Inline ConnectionStatus literal unions in multiple components (e.g. ServerStatusIndicator, ServerCard, ConnectedView) → import ConnectionStatus from core/mcp/types.ts.
  • Inline "stdio" | "http" transport unions (e.g. TransportBadge) → ServerType from core/mcp/types.ts.

Output of 0.3 lives in the PR description, not in this file.

Phase 1 — Elements (16 components)

Smallest blast radius, fewest dependencies, and the type changes here propagate up. Do this first so groups can immediately consume the new element shapes.

Strategy

  • One commit per element (or one commit per logical batch of 2–3 closely related elements). Each commit refactors <Name>.tsx, updates <Name>.stories.tsx, and runs format/lint/build.
  • For each element, the steps are mechanical:
    1. Read v2_ux_interfaces.md Section 1's entry for the component.
    2. Replace the current props interface with the Target props from the spec.
    3. Update the body to read fields off the new prop shape.
    4. Update the story args to construct realistic schema fixtures (use real LoggingLevel strings, real Annotations objects, etc.).
    5. Run npm run format && npm run lint && npm run build.

Order (rough dependency order)

  1. Pure presentational, no behavior change (warm-up): CopyButton, ListToggle, ListChangedIndicator, SubscribeButton — spec says "unchanged" or trivial.
  2. Schema-typed primitives that other elements depend on: LogLevelBadge (introduces LoggingLevel), TransportBadge (introduces InspectorTransportType), TaskStatusBadge (introduces InspectorTaskStatus), ServerStatusIndicator (introduces ConnectionStatus).
  3. Components consuming MCP wrapper objects: AnnotationBadge (consumes Annotations/ToolAnnotations), CapabilityItem (consumes keyof ServerCapabilities), LogEntry (consumes LoggingMessageNotification['params'] + receivedAt), MessageBubble (consumes SamplingMessage/PromptMessage), ProgressDisplay (consumes ProgressNotification['params']).
  4. Composite-input elements: ContentViewer (consumes the full ContentBlock discriminated union), InlineError (consumes JSONRPCError['error']), ConnectionToggle (split single onChange into onConnect/onDisconnect).

Validation per element

  • npm run format && npm run lint && npm run build (this is the project rule in AGENTS.md).
  • Open the element's story in Storybook locally; visual output should be unchanged unless the spec calls out a new visible field.

Phase 1 checkpoint

  • Single commit on the branch summarizing the elements pass, OR a stack of smaller commits — preference: small commits.
  • Before moving to Phase 2, scan clients/web/src/components/elements/ for any remaining local-union literal types that should have been replaced. Delete the dead local types.

Phase 2 — Groups (37 components)

The bulk of the schema surface flows through this layer. Most groups embed 1–3 elements that already changed in Phase 1, so doing groups after Phase 1 means we only touch each file once.

Strategy

  • One PR per logical group cluster (not one PR per component) — clusters below. Each PR refactors all components in the cluster, updates their stories, and runs format/lint/build.
  • For groups whose props are mostly a flat copy of an MCP schema object (ToolListItem, ResourceListItem, PromptListItem, ToolControls, PromptControls, ResourceControls, RootsTable, LogStreamPanel, LogControls), the refactor is mechanical: import the schema type, swap the local prop name, update story fixtures.
  • For groups that wrap a single full panel of state (ToolDetailPanel, ResourcePreviewPanel, ResourceTemplatePanel, SamplingRequestPanel, ElicitationFormPanel, ElicitationUrlPanel, PromptArgumentsForm), the refactor moves multiple flat scalar props into a single schema or wrapper object and updates internal destructuring.
  • For groups that flag a dumb-component principle violation (notably ServerCard's auto-connect useEffect), the violation is fixed in the same commit that refactors the props — do not let it linger.

Cluster order (within Phase 2)

  1. Tools clusterToolListItem, ToolControls, ToolDetailPanel, ToolResultPanel. All consume Tool / CallToolRequest / CallToolResult / ToolAnnotations.
  2. Prompts clusterPromptListItem, PromptControls, PromptArgumentsForm, PromptMessagesDisplay. All consume Prompt / PromptArgument / GetPromptResult / PromptMessage.
  3. Resources clusterResourceListItem, ResourceControls, ResourcePreviewPanel, ResourceTemplatePanel, ResourceSubscribedItem. Consume Resource / ResourceTemplate / ReadResourceResult / Text|BlobResourceContents / Annotations.
  4. Logs clusterLogControls, LogStreamPanel. Consume LoggingLevel, LoggingMessageNotification['params'].
  5. Tasks clusterTaskCard, TaskControls, TaskListPanel. Introduce InspectorTask wrapper.
  6. History clusterHistoryControls, HistoryEntry, HistoryListPanel. Introduce InspectorHistoryEntry wrapper.
  7. Sampling / elicitation clusterSamplingRequestPanel, InlineSamplingRequest, ElicitationFormPanel, ElicitationUrlPanel, InlineElicitationRequest, PendingClientRequests. Consume CreateMessageRequest/Result, ElicitRequest/Result, InspectorUrlElicitRequest, InspectorPendingRequest.
  8. Roots / Schema / Experimental clusterRootsTable (consumes Root), SchemaForm (consumes shared JSON Schema type), ExperimentalFeaturesPanel (consumes ServerCapabilities['experimental'], ClientCapabilities['experimental'], JSONRPCRequest/Response/Error).
  9. Server / settings / view-shell clusterServerCard (also lifts the auto-connect useEffect), ServerInfoContent, ServerSettingsForm, ServerListControls, ServerAddMenu, ImportServerJsonPanel, ViewHeader. Consume Implementation, InitializeResult, ServerCapabilities, ClientCapabilities, InspectorServerConfig, InspectorServerSettings, InspectorOAuthDetails, InspectorServerJsonDraft, InspectorTransportType.

Validation per cluster

  • npm run format && npm run lint && npm run build after each cluster.
  • Spot-check the corresponding Storybook stories visually.
  • After every cluster, run a tsc --noEmit pass over the whole web client to catch consumers in screens/views that are still passing the old shape. Those will be temporarily broken — that's expected; screens/views are Phase 3 / Phase 4. To keep the build green during the transition, screens pass props through as unknown as ... adapter casts that get deleted in Phase 3. Alternative: do Phase 3 alongside each cluster — see below.

Inter-phase coupling decision

There are two ways to keep the build green while groups are mid-refactor:

  1. Adapter shims in the consuming screen for the duration of the cluster, deleted in Phase 3. Pro: clean per-cluster commits. Con: noisy diffs in screens that get rewritten anyway.
  2. Refactor each group cluster's parent screen in the same PR. Pro: no shims, no temporarily-broken builds. Con: bigger PRs, but those PRs are the natural unit of work anyway because each cluster maps almost 1:1 to one screen.

Proposed: option 2. When the Tools group cluster is refactored, the ToolsScreen is refactored in the same PR. This collapses Phase 2 and Phase 3 into a single per-feature pass and keeps npm run build green at every commit.

Phase 3 — Screens (7 components, folded into Phase 2)

Per the decision above, each screen is refactored in the same PR as its group cluster:

  • ToolsScreen ← Tools cluster
  • PromptsScreen ← Prompts cluster
  • ResourcesScreen ← Resources cluster
  • LoggingScreen ← Logs cluster
  • TasksScreen ← Tasks cluster
  • HistoryScreen ← History cluster
  • ServerListScreen ← Server / settings / view-shell cluster

For each screen:

  1. Replace ad hoc list-of-*Props props with <SchemaType>[] arrays.
  2. Move selection from a pre-built selected-detail prop (selectedTool: ToolDetailPanelProps) to a selected name/uri/id (selectedToolName: string) and derive the detail object internally via tools.find(...).
  3. Replace the separate result / messages props with a unified callState / getPromptState / readState discriminated union so the detail panel can render pending and error states uniformly.
  4. Local UI state (search text, filter toggles, compact mode, selected name) stays in useState inside the screen.
  5. Update the screen's stories — the existing stories carry the burden of exercising every prop combination. They will need new fixtures because they currently pass a flattened shape that no longer compiles.

Sampling / elicitation cluster note

Sampling and elicitation flows do not have a dedicated screen — they appear inline (InlineSamplingRequest, InlineElicitationRequest) or as modal panels invoked from other screens (SamplingRequestPanel, ElicitationFormPanel, ElicitationUrlPanel). Their cluster ends with updates to the stories that mount them in modals (UnconnectedView ⇒ WithSettingsModal-style and ConnectedView ⇒ ToolsWithElicitation*Modal-style existing stories) rather than a screen refactor.

Phase 4 — Views (2 components)

Trivial after groups + screens. Finishes the refactor.

ConnectedView

  • Replace flat scalars serverName, status, latencyMs, activeTab, availableTabs with: serverInfo: Implementation, capabilities: ServerCapabilities, connectionStatus: ConnectionStatus, latencyMs?: number, activeTab: InspectorTab, availableTabs: InspectorTab[].
  • Compute availableTabs against capabilities rather than accepting an arbitrary string list.
  • Forward serverInfo to the now-refactored ViewHeader.

UnconnectedView

  • Rename internal HomeLayoutProps interface to UnconnectedViewProps.
  • No prop changes needed.
  • Optional: add connectionStatus if the embedded header needs it.

4.3 Move selection state into the screens

The Phase 2 cluster work threaded selectedXxxName / onSelectXxx props through every list-and-detail screen, leaving selection as caller-owned state. That choice contradicts Phase 3's "selected name stays in useState inside the screen" rule and creates a real wiring gap: nothing above the screen has any reason to know which tool/prompt/resource the user clicked on, so under InspectorView clicks would have to be round-tripped through App.tsx purely to get the detail panel to render.

Selection is screen-local UI state. MCP-derived data (callState / fetched prompt messages / resource read results) keeps flowing in from InspectorView as props. Refactor each screen accordingly.

Screens to fix:

  • ToolsScreen — drop selectedToolName and onSelectTool from the prop interface; track the selected tool name in useState inside the screen. ToolControls continues to receive selectedName and onSelectTool as props from the screen. callState (and any future result/progress payload) keeps coming in from InspectorView. InspectorView only needs to know the selected tool name when it triggers onCallTool — which the screen already owns, so it can pass the name up at call time rather than maintaining it in the parent.
  • PromptsScreen — same pattern for selectedPromptName. Fetched GetPromptResult continues to arrive as a prop from InspectorView.
  • ResourcesScreen — same pattern for the selected resource URI and selected resource template URI. ReadResourceResult continues to arrive as a prop.

For each screen:

  1. Remove the selectedXxxName / onSelectXxx props from the screen's prop interface.
  2. Add a useState for the selection inside the screen and pass it down to the controls group.
  3. When the user triggers a request (onCallTool / onGetPrompt / onReadResource), pass the currently selected name/uri up alongside any other arguments — the parent does not need to track selection between calls.
  4. Update each screen's stories to remove the now-deleted props. The stories will start working without useArgs selection wiring, because selection is now genuinely internal.
  5. Audit the controls groups (ToolControls, PromptControls, ResourceControls) — their selectedName / onSelectXxx props are correct (they ARE caller-owned, because the screen owns them) and should not change.
  6. Update InspectorView and any wiring fixtures to drop the now-removed selection props they were passing to each screen.

Phase 5 — Cleanup pass

After all four phases, sweep:

  1. Delete every local type listed in Phase 0.3 that should have disappeared.
  2. Run npm run format && npm run lint && npm run build one last time.
  3. git grep for JsonSchema, LogLevel, TaskStatus, RootEntry, ToolListItemProps, PromptItem, ResourceItem, SubscriptionItem, TemplateListItem, ServerCardProps as a data shape — confirm zero matches outside of intentional kept types.
  4. Update clients/web/README.md if there's anything in it that refers to the now-deleted shapes.
  5. Create a follow-up issue (or PR placeholder) for the v2 core/ package migration: lift clients/web/src/types/inspector/ into core/types/, update all import paths.

Validation strategy (whole-project)

After each phase:

  • cd clients/web && npm run format && npm run lint && npm run build
  • Visually open Storybook (npm run storybook) and click through the refactored components.
  • For groups/screens that introduced or substantially changed a wrapper type, write at least one Storybook story that uses a fixture clearly derived from a real MCP SDK type (e.g. import Tool from the SDK in the story file and construct one — this catches type drift instantly).

After the whole effort:

  • Read specification/v2_ux_interfaces.md end to end and confirm every component's "Internal refactors" bullet is satisfied. Mark done.
  • The follow-up "build the v2 core hook layer" effort can begin with this branch's component contracts as its input spec.

Risks and open questions

  1. JSON Schema typing. Resolved. Copy v1.5's clients/web/src/utils/jsonUtils.ts (JsonValue, JsonObject, JsonSchemaType) and clients/web/src/utils/schemaUtils.ts (AJV-backed Tool.outputSchema validation) verbatim. SchemaForm switches from its local JsonSchema type to JsonSchemaType.
  2. PrimitiveSchemaDefinition for elicitation. ElicitRequest's requestedSchema is constrained to primitives. The current SchemaForm handles arbitrary schemas. The fix is either two form components or one form component with a primitive-only mode flag — decide during the sampling/elicitation cluster. Check what v1.5 does first; v1.5 already supports both form-mode and URL-mode elicitation per InspectorClientOptions.elicit so the constraint is already represented.
  3. tools / toolChoice on SamplingRequestPanel. These fields are not in CreateMessageRequest in MCP 2025-11-25. Per the spec, the choice is "Inspector extension or drop". Default proposal: drop them; revisit if a future MCP revision adds them. Verify v1.5's core/mcp/samplingCreateMessage.ts does or does not surface these fields before deciding.
  4. Task is in the SDK 1.x — interfaces doc is partially stale. The v2_ux_interfaces.md text says "MCP 2025-11-25 base schema doesn't have a first-class task primitive". This is no longer correct: MCP TS SDK 1.x exports Task from @modelcontextprotocol/sdk/types.js and v1.5 already uses it as a raw type via useManagedRequestorTasks. Action: during Phase 0, post a follow-up commit that updates the TaskStatusBadge, TaskCard, TaskListPanel, and TasksScreen entries in v2_ux_interfaces.md to reference Task directly instead of the InspectorTask placeholder. Do not silently diverge.
  5. InspectorClientOptions boundary. v1.5's InspectorClientOptions is a construction-time options bag for InspectorClient, not a settings object the user edits at runtime. The ServerSettingsForm props need a runtime subset (timeouts, OAuth client id/secret/scope, headers, metadata, custom instructions, feature flags). Create a small InspectorServerSettings wrapper in core/mcp/types.ts that picks exactly those fields off InspectorClientOptions — do not invent a parallel hierarchy.
  6. Story bundle size. The existing ConnectedView.stories.tsx is already long because each story inlines a full screen of fixture data. Refactoring each story to use real schema types will make them longer. Consider factoring fixtures into shared *.fixtures.ts files alongside the component, especially for tools / prompts / resources where the same object set is reused across multiple stories. Decide per cluster.
  7. Wrapper type names. Use v1.5's names verbatim, not the Inspector* placeholders from v2_ux_interfaces.md. The "Name-mapping correction" table at the top of this plan is the authoritative rename list. Update v2_ux_interfaces.md after Phase 0 so the spec and the implementation agree on names.
  8. Date vs. string for timestamps. v1.5's MessageEntry, StderrLogEntry, FetchRequestEntry, and the *Invocation types use Date, not ISO strings. The current Storybook stories pass ISO strings. When refactoring, convert fixtures to new Date(...). Stories' visible output should not change because Date.toISOString() / toLocaleString() is invoked at render time anyway.
  9. Visual regressions. The refactor is meant to be visually invisible, but several elements get new visible fields (ResourceListItem will start rendering Annotations it currently ignores; LogEntry will render params.data directly). Capture before/after Storybook screenshots for each cluster's PR description so reviewers can sign off.
  10. v1.5 has more state machinery than this plan touches. v1.5 ships core/mcp/state/managed{Tools,Prompts,Resources,ResourceTemplates,RequestorTasks}State.ts plus paged*State.ts siblings, plus core/react/useManaged*.ts hooks that subscribe to them via EventTarget. None of that is in scope for this plan — only the types are. The hook layer build-out is the follow-up effort that will consume the contracts produced here.
  11. v2_ux_interfaces.md was produced with unverified hedges. The doc contains 52 instances of (likely) because the parallel research subagents that filled in each section were instructed to prefix uncertain hook names with the hedge rather than fail to fill in a section. Several of those hedges turned out to be wrong (see Phase 0.3 rename table). The rule going forward for any planning / spec doc in this repo: hedging words like likely, probably, presumably, should be next to a type name, hook name, or file path are an unverified claim. Either resolve them against the source of truth (v1.5 branch of this repo for Inspector-owned types/hooks/wrappers; @modelcontextprotocol/sdk/types.js for protocol schema types) before the doc ships, or mark them explicitly as not present in v1.5 — needs design. Do not leave a hedge in a planning doc and treat it as research. The Phase 0.3 step is the immediate cleanup; the rule is the durable preference.

Out of this plan, but adjacent

  • Vitest unit tests for each refactored component (90% per AGENTS.md).
  • Building the v2 core/ hook layer. v1.5 already has the full hook surface (useInspectorClient, useManagedTools, useManagedPrompts, useManagedResources, useManagedResourceTemplates, useManagedRequestorTasks, useMessageLog, useStderrLog, useFetchRequestLog, plus usePaged* siblings) and the state machinery it subscribes to (core/mcp/state/managed*State.ts, core/mcp/state/paged*State.ts). Porting that layer wholesale to v2 is a separate effort that will consume the component contracts produced here as its input spec.
  • Wiring App.tsx to the v2 core hook layer.
  • CLI / TUI clients. v1.5 has both (clients/cli/, clients/tui/); v2 will reuse the wrapper types from core/mcp/types.ts once that file exists in v2.
  • Updating v2_ux_interfaces.md to use v1.5's actual wrapper-type names (MCPServerConfig, MessageEntry, Task, etc.) and v1.5's actual hook names (useManagedTools, useInspectorClient, useMessageLog, etc.) instead of the Inspector* and (likely) placeholders. Done as part of Phase 0.3 in this plan, not a follow-up.