Inspector V2 Tech Stack - Storage Specification

July 5, 2026 · View on GitHub

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

Web Client | CLI, TUI, Launcher | Server | Storage

Overview | Server List File

Overview

This specification defines the storage and state management architecture for Inspector V2. It addresses how data is persisted, where state lives, and which libraries manage it.

Design Principles

  1. Interface-first - Repository/service interfaces remain storage-agnostic
  2. Core stays React-free - No React dependencies in @modelcontextprotocol/inspector-core
  3. Hybrid approach - Different state management for different concerns
  4. Server configs via proxy - Per #1805, NOT browser localStorage

State Categories

CategoryExamplesPersistenceLocation
OAuth runtime stateTokens, PKCE, scopes, IdP sessions, AS metadataFile (oauth.json)OAuthStorageBase + persist backends
Server ConfigurationsURL, transport, headersFile (mcp.json)Proxy server
User PreferencesTheme, log level, panel sizeslocalStorageBrowser
Connection StateStatus, server info, errorsMemoryReact Context
Execution StateCurrent request, pending queueMemoryReact Context
Logs DisplayFiltered view, pause stateMemorycore/mcp/state + React hooks
Execution Form StateSelected tool, form valuesMemoryReact component state / hooks
Testing ProfilesCustom profiles, active selectionlocalStorageBrowser (planned)
History DataRequest/response recordsNDJSON file (Pino)Proxy server

Server-Side vs Client-Side Storage

This spec focuses on client-side state management. Server-side persistence uses a different stack:

LayerTechnologyPurpose
Proxy (Pino)NDJSON filesRaw history persistence (history.ndjson), parsed by History API
Client (browser UI)localStorage + memoryUI preferences, display buffers

The Pino logger on the proxy writes MCP request/response records to NDJSON format. The History API endpoint parses this file and returns filtered JSON. Client-side core/mcp/state stores and React hooks handle:

  • How logs are displayed (filters, pause, auto-scroll)
  • Caching fetched history for UI performance
  • User preferences that don't belong on the server

See v2_server.md for Pino configuration details.


OAuth runtime persistence

OAuth and EMA runtime state (access/refresh tokens, PKCE verifiers, granted scopes, cached authorization-server metadata, IdP sessions) is stored separately from mcp.json in ~/.mcp-inspector/storage/oauth.json by default.

ClientImplementationPath
WebRemoteOAuthStorage/api/storage/oauthSame on-disk file via Hono backend
CLI / TUINodeOAuthStorageDirect file I/O
Tests / referenceBrowserOAuthStoragesessionStorage (not wired in v2 web app)

Stack (#1549): OAuthStorage interface → OAuthStorageBase + OAuthMemoryStore + OAuthPersistBackend (core/auth/oauth-storage.ts, core/auth/store.ts, core/auth/oauth-persist.ts). Writes plain JSON { servers, idpSessions }. Reads still promote legacy { state, version } envelopes (migrate-on-write). All getters are async; setters auto-persist. Web shares one store via getWebRemoteOAuthStorage().

Details: EMA auth, Mid-session auth.


State Management Options Analysis

Comparison Matrix

CriteriaZustandRedux/RTKJotaiContext+useReducerTanStack QueryValtio
Bundle Size~1.2KB~11KB~2.2KB0 (built-in)~13KB~3KB
Learning CurveLowMedium-HighLowLowMediumLow
BoilerplateMinimalHighMinimalMediumLowMinimal
DevToolsYesExcellentYesReact DevToolsYesYes
Persist MiddlewareBuilt-inRTK-persistYesManualN/AYes
React-free UsageYes (/vanilla)NoNoNoNoYes
TypeScript SupportExcellentExcellentGoodGoodExcellentGood
Selector OptimizationBuilt-inRequires memoizationAutomaticManualAutomaticAutomatic

Detailed Analysis

Pros:

  • Minimal boilerplate - no actions, reducers, or providers for simple stores
  • Built-in persist middleware for automatic localStorage synchronization
  • Selector-based access prevents unnecessary re-renders
  • React-independent core (zustand/vanilla) supports CLI/TUI reuse
  • Excellent TypeScript inference without verbose type annotations
  • MCPJam Inspector precedent demonstrates proven patterns in comparable application

Cons:

  • Less structured than Redux for complex state transitions
  • No built-in time-travel debugging (devtools extension available)
  • Multiple stores can fragment state if not carefully organized

Verdict: Recommended for user preferences, logs display, execution form state, testing profiles

Redux / Redux Toolkit

Pros:

  • Excellent DevTools with time-travel debugging
  • Very structured, predictable state updates via reducers
  • Large ecosystem and community support
  • RTK simplifies traditional Redux boilerplate significantly

Cons:

  • Heavy bundle size (11KB) for Inspector's scope
  • Overkill for single-server connection model
  • Requires React bindings (react-redux) - violates React-free core
  • Higher cognitive overhead for simple state

Verdict: Rejected - overkill for Inspector's requirements

Jotai

Pros:

  • Atomic state model enables granular updates
  • Minimal re-renders by design
  • Good for derived/computed state
  • Suspense integration for async state

Cons:

  • Less intuitive for imperative state updates
  • Smaller ecosystem than Zustand

Verdict: Good alternative, but less proven in MCP tooling context

Context + useReducer

Pros:

  • No additional dependencies (built into React)
  • Clear action-based state transitions
  • Familiar pattern for React developers
  • Good for truly global, infrequently-updated state

Cons:

  • Re-renders all consumers on any state change without careful splitting
  • Manual selector optimization required (useMemo, useCallback)
  • Verbose for multiple state domains
  • No built-in persistence

Verdict: Keep for connection/execution state machines (already implemented)

TanStack Query (React Query)

Pros:

  • Excellent for server state caching and synchronization
  • Built-in caching, background refetch, optimistic updates
  • Devtools for debugging async state
  • Handles loading/error states elegantly

Cons:

  • Designed for async server state, not client UI state
  • Not appropriate for local-only state like preferences
  • Additional complexity for Inspector's proxy-mediated data fetching

Verdict: Consider as complement for History/Logs API calls when repository uses proxy

Valtio

Pros:

  • Proxy-based mutable API feels natural for imperative updates
  • Automatic re-render optimization
  • Can be used outside React

Cons:

  • Proxy "magic" can be confusing for debugging
  • Less explicit state updates than Zustand
  • Smaller community adoption

Verdict: Viable alternative, but proxy semantics less familiar to team


Hybrid Approach

State DomainTechnologyJustification
Connection StateuseReducer + ContextClear state machine (disconnected -> connecting -> connected -> error); already implemented in McpContext
Execution StateuseReducer + ContextComplex transitions with pending requests; already implemented in ExecutionContext
User PreferencesZustand + persistSimple key-value; needs persistence; avoids prop drilling
Logs DisplayZustandReal-time buffer; filter state; pause/resume
Execution FormsZustandForm values; selected items; last result display
Testing ProfilesZustand + persistUser configurations; active selection
Server ConfigsRepository (proxy API)Per spec constraint; not browser storage
History DataRepository interfaceStorage implementation deferred

Why NOT Replace useReducer for Connection State?

Connection state is a clear state machine with well-defined transitions:

disconnected -> connecting -> connected -> error
     ^                            |          |
     |____________________________|__________|

useReducer excels at action-based transitions:

  • CONNECT_REQUEST - Begin connection attempt
  • CONNECT_SUCCESS - Store server info, capabilities
  • CONNECT_ERROR - Store error details
  • DISCONNECT - Clean up and reset

This pattern is already implemented in McpContext.tsx and working well. No migration needed.


Zustand Store Specifications

Historical note: This section captured an early plan to use Zustand for browser UI state. The shipped web client uses core/mcp/state event-driven stores and core/react hooks instead. OAuth runtime persistence uses OAuthStorageBase (see OAuth runtime persistence above), not Zustand. The comparison matrix below remains useful for evaluating future UI-state libraries.

1. Preferences Store

Purpose: Persist user preferences across sessions

Persistence: localStorage via persist middleware

interface PreferencesState {
  // Theme
  theme: 'light' | 'dark' | 'system';

  // Logging
  logLevel: LogLevel;
  showTimestamps: boolean;
  wrapLogLines: boolean;

  // Display
  compactMode: boolean;
  showAnnotations: boolean;

  // Layout
  logsExpanded: boolean;
  historySidebarWidth: number;
}

interface PreferencesActions {
  setTheme: (theme: PreferencesState['theme']) => void;
  setLogLevel: (level: LogLevel) => void;
  toggleTimestamps: () => void;
  toggleWrapLines: () => void;
  toggleCompactMode: () => void;
  toggleAnnotations: () => void;
  setLogsExpanded: (expanded: boolean) => void;
  setHistorySidebarWidth: (width: number) => void;
  resetToDefaults: () => void;
}

localStorage key: inspector-preferences

2. Logs Display Store

Purpose: Manage real-time log display state

Persistence: None (ephemeral)

interface LogsDisplayState {
  // Buffer (in-memory only)
  entries: LogEntry[];

  // Filters
  minLevel: LogLevel;
  loggerFilter: string | null;
  requestIdFilter: string | null;
  searchQuery: string;

  // Controls
  isPaused: boolean;
  isAutoScroll: boolean;
}

interface LogsDisplayActions {
  addEntry: (entry: LogEntry) => void;
  addBatch: (entries: LogEntry[]) => void;
  clearLogs: () => void;

  setMinLevel: (level: LogLevel) => void;
  setLoggerFilter: (logger: string | null) => void;
  setRequestIdFilter: (requestId: string | null) => void;
  setSearchQuery: (query: string) => void;

  togglePause: () => void;
  toggleAutoScroll: () => void;
}

Memory cap: 1000 entries (FIFO eviction)

3. Execution Form Store

Purpose: Track tool/resource/prompt execution form state

Persistence: None (ephemeral)

interface ExecutionFormState {
  // Tools
  selectedToolName: string | null;
  toolFormValues: Record<string, unknown>;
  lastToolResult: ToolResult | null;

  // Resources
  selectedResourceUri: string | null;
  resourceContent: unknown | null;

  // Prompts
  selectedPromptName: string | null;
  promptFormValues: Record<string, string>;
  promptMessages: unknown[] | null;
}

interface ToolResult {
  toolName: string;
  args: Record<string, unknown>;
  result: unknown;
  timestamp: string;
  duration: number;
  isError: boolean;
}

interface ExecutionFormActions {
  selectTool: (name: string | null) => void;
  setToolFormValues: (values: Record<string, unknown>) => void;
  setLastToolResult: (result: ToolResult | null) => void;

  selectResource: (uri: string | null) => void;
  setResourceContent: (content: unknown | null) => void;

  selectPrompt: (name: string | null) => void;
  setPromptFormValues: (values: Record<string, string>) => void;
  setPromptMessages: (messages: unknown[] | null) => void;

  reset: () => void;
}

Note: Full execution history is persisted server-side via Pino/NDJSON and accessed through the History API. The lastToolResult field provides immediate display of the most recent result without duplicating the history store.

4. Testing Profiles Store

Purpose: Manage sampling/elicitation response configurations

Persistence: localStorage via persist middleware

interface TestingProfilesState {
  profiles: TestingProfile[];
  activeProfileId: string;
}

interface TestingProfilesActions {
  setActiveProfile: (id: string) => void;
  addProfile: (profile: Omit<TestingProfile, 'id'>) => TestingProfile;
  updateProfile: (id: string, updates: Partial<TestingProfile>) => void;
  deleteProfile: (id: string) => void;
  resetToDefaults: () => void;
}

localStorage key: inspector-testing-profiles

Default profiles:

  1. Manual - Respond to requests manually (no auto-approve)
  2. Auto-Approve - Automatically approve with default response

Integration with Core Package

Repository Interface Pattern

Zustand stores in the client package wrap core repository interfaces, not replace them:

// client/src/stores/historyStore.ts
import type { HistoryRepository } from '@modelcontextprotocol/inspector-core';

export function createHistoryStore(repository: HistoryRepository) {
  return create<HistoryStoreState>((set, get) => ({
    entries: [],
    isLoading: false,

    fetch: async () => {
      set({ isLoading: true });
      const entries = await repository.list();
      set({ entries, isLoading: false });
    },

    add: async (entry) => {
      const added = await repository.add(entry);
      set((s) => ({ entries: [added, ...s.entries] }));
      return added;
    },
    // ... other methods delegate to repository
  }));
}

This pattern:

  • Keeps core interfaces unchanged
  • Allows swapping repository implementations (memory, proxy API, file)
  • Provides reactive state for UI components
  • Maintains testability with memory stubs

Dependency Flow

+------------------------------------------------------------------+
|                        Client Package                             |
|                                                                   |
|  +------------------+    +---------------------+                  |
|  | Zustand Stores   |    | Context Providers   |                  |
|  |  - preferences   |    |  - McpContext       |                  |
|  |  - logsDisplay   |    |  - ExecutionContext |                  |
|  |  - executionForm |    +---------------------+                  |
|  |  - testingProfiles      |                                      |
|  +--------+---------+      |                                      |
|           |                |                                      |
|           v                v                                      |
|  +--------------------------------------------------------+      |
|  |              Core Package Interfaces                    |      |
|  |  - ServerConfigRepository  - HistoryRepository          |      |
|  |  - LogsRepository          - TestingProfileRepository   |      |
|  +------------------------+-------------------------------+      |
+---------------------------|-----------------------------------+
                            |
                            v
            +-------------------------------+
            |    Core Package (React-free)  |
            |  - MCP Client lifecycle       |
            |  - Transport creation         |
            |  - Handler functions          |
            |  - Type definitions           |
            +-------------------------------+

File Structure

client/src/stores/
  index.ts                    # Re-exports all stores
  preferencesStore.ts         # Theme, log level, display prefs
  logsDisplayStore.ts         # Log buffer, filters, controls
  executionFormStore.ts       # Tool/resource/prompt form state
  testingProfilesStore.ts     # Sampling/elicitation profiles

Migration Path

Phase 1: Add Zustand (Non-Breaking)

  1. Install Zustand: npm install zustand
  2. Create client/src/stores/ directory
  3. Implement all four stores
  4. No changes to existing components yet

Phase 2: Migrate Components

  1. Replace mockTestingProfiles in ExecutionContext with store
  2. Update preference-dependent components (theme, log settings)
  3. Update logs page to use logsDisplay store
  4. Update tools/resources/prompts pages to use execution form store

Phase 3: Connect Repository Layer

  1. When storage implementation is decided, create repository implementations
  2. Wrap repositories with Zustand store factory pattern
  3. No UI component changes required

Open Questions

  1. TanStack Query complement? Should we add TanStack Query for History/Logs API calls when repositories use proxy API? This would provide caching, background refresh, and optimistic updates.

  2. Devtools in production? Should Zustand devtools be enabled only in development builds, or also in production for debugging?

  3. Store granularity? Should execution form store be split into toolsStore, resourcesStore, promptsStore for finer control?


References