Brief | V1 Problems | V2 Scope | V2 Tech Stack | V2 UX
Web Client | CLI, TUI, Launcher | Server | Storage
Overview | Server List File
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.
- Interface-first - Repository/service interfaces remain storage-agnostic
- Core stays React-free - No React dependencies in
@modelcontextprotocol/inspector-core - Hybrid approach - Different state management for different concerns
- Server configs via proxy - Per #1805, NOT browser localStorage
| Category | Examples | Persistence | Location |
|---|---|---|---|
| Server Configurations | URL, transport, headers | File (mcp.json) | Proxy server |
| User Preferences | Theme, log level, panel sizes | localStorage | Browser |
| Connection State | Status, server info, errors | Memory | React Context |
| Execution State | Current request, pending queue | Memory | React Context |
| Logs Display | Filtered view, pause state | Memory | Zustand store |
| Execution Form State | Selected tool, form values | Memory | Zustand store |
| Testing Profiles | Custom profiles, active selection | localStorage | Zustand store |
| History Data | Request/response records | NDJSON file (Pino) | Proxy server |
This spec focuses on client-side state management. Server-side persistence uses a different stack:
| Layer | Technology | Purpose |
|---|---|---|
| Proxy (Pino) | NDJSON files | Raw history persistence (history.ndjson), parsed by History API |
| Client (Zustand) | localStorage + memory | UI state, display buffers, user preferences |
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 Zustand stores 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.
| Criteria | Zustand | Redux/RTK | Jotai | Context+useReducer | TanStack Query | Valtio |
|---|---|---|---|---|---|---|
| Bundle Size | ~1.2KB | ~11KB | ~2.2KB | 0 (built-in) | ~13KB | ~3KB |
| Learning Curve | Low | Medium-High | Low | Low | Medium | Low |
| Boilerplate | Minimal | High | Minimal | Medium | Low | Minimal |
| DevTools | Yes | Excellent | Yes | React DevTools | Yes | Yes |
| Persist Middleware | Built-in | RTK-persist | Yes | Manual | N/A | Yes |
| React-free Usage | Yes (/vanilla) |
No | No | No | No | Yes |
| TypeScript Support | Excellent | Excellent | Good | Good | Excellent | Good |
| Selector Optimization | Built-in | Requires memoization | Automatic | Manual | Automatic | Automatic |
Pros:
- Minimal boilerplate - no actions, reducers, or providers for simple stores
- Built-in
persistmiddleware 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
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
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
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)
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
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
| State Domain | Technology | Justification |
|---|---|---|
| Connection State | useReducer + Context | Clear state machine (disconnected -> connecting -> connected -> error); already implemented in McpContext |
| Execution State | useReducer + Context | Complex transitions with pending requests; already implemented in ExecutionContext |
| User Preferences | Zustand + persist | Simple key-value; needs persistence; avoids prop drilling |
| Logs Display | Zustand | Real-time buffer; filter state; pause/resume |
| Execution Forms | Zustand | Form values; selected items; last result display |
| Testing Profiles | Zustand + persist | User configurations; active selection |
| Server Configs | Repository (proxy API) | Per spec constraint; not browser storage |
| History Data | Repository interface | Storage implementation deferred |
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 attemptCONNECT_SUCCESS- Store server info, capabilitiesCONNECT_ERROR- Store error detailsDISCONNECT- Clean up and reset
This pattern is already implemented in McpContext.tsx and working well. No migration needed.
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
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)
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.
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:
- Manual - Respond to requests manually (no auto-approve)
- Auto-Approve - Automatically approve with default response
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
+------------------------------------------------------------------+
| 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 |
+-------------------------------+
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
- Install Zustand:
npm install zustand - Create
client/src/stores/directory - Implement all four stores
- No changes to existing components yet
- Replace
mockTestingProfilesin ExecutionContext with store - Update preference-dependent components (theme, log settings)
- Update logs page to use logsDisplay store
- Update tools/resources/prompts pages to use execution form store
- When storage implementation is decided, create repository implementations
- Wrap repositories with Zustand store factory pattern
- No UI component changes required
-
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.
-
Devtools in production? Should Zustand devtools be enabled only in development builds, or also in production for debugging?
-
Store granularity? Should execution form store be split into toolsStore, resourcesStore, promptsStore for finer control?
- Issue #983 - Data spec discussion
- Discussion #1805 - Server config storage decision
- MCPJam Inspector - Reference implementation using Zustand
- Zustand documentation