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
- 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
State Categories
| Category | Examples | Persistence | Location |
|---|---|---|---|
| OAuth runtime state | Tokens, PKCE, scopes, IdP sessions, AS metadata | File (oauth.json) | OAuthStorageBase + persist backends |
| 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 | core/mcp/state + React hooks |
| Execution Form State | Selected tool, form values | Memory | React component state / hooks |
| Testing Profiles | Custom profiles, active selection | localStorage | Browser (planned) |
| History Data | Request/response records | NDJSON 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:
| Layer | Technology | Purpose |
|---|---|---|
| Proxy (Pino) | NDJSON files | Raw history persistence (history.ndjson), parsed by History API |
| Client (browser UI) | localStorage + memory | UI 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.
| Client | Implementation | Path |
|---|---|---|
| Web | RemoteOAuthStorage → /api/storage/oauth | Same on-disk file via Hono backend |
| CLI / TUI | NodeOAuthStorage | Direct file I/O |
| Tests / reference | BrowserOAuthStorage | sessionStorage (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
| 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 |
Detailed Analysis
Zustand (Recommended for UI State)
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
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
Recommended Architecture
Hybrid Approach
| 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 |
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 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.
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/stateevent-driven stores andcore/reacthooks instead. OAuth runtime persistence usesOAuthStorageBase(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:
- Manual - Respond to requests manually (no auto-approve)
- 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)
- Install Zustand:
npm install zustand - Create
client/src/stores/directory - Implement all four stores
- No changes to existing components yet
Phase 2: Migrate Components
- 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
Phase 3: Connect Repository Layer
- When storage implementation is decided, create repository implementations
- Wrap repositories with Zustand store factory pattern
- No UI component changes required
Open Questions
-
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?
References
- Issue #983 - Data spec discussion
- Discussion #1805 - Server config storage decision
- MCPJam Inspector - Reference implementation using Zustand
- Zustand documentation