From ce39e0de7c08e01784d8fa2b52d4a022bf4cf2e1 Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 19 Jan 2026 07:05:27 -0700 Subject: [PATCH 1/4] spec: Add v2 storage specification proposing Zustand Closes #983 Proposes a hybrid state management architecture: - Zustand for UI preferences, logs display, playground state, testing profiles - Keep useReducer + Context for connection/execution state machines - Repository interfaces remain storage-agnostic for proxy API integration Includes detailed analysis of storage options (Zustand, Redux, Jotai, Context+useReducer, TanStack Query, Valtio) with pros/cons comparison. Co-Authored-By: Claude Opus 4.5 --- specification/v2_storage.md | 485 ++++++++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 specification/v2_storage.md diff --git a/specification/v2_storage.md b/specification/v2_storage.md new file mode 100644 index 000000000..29ac6042a --- /dev/null +++ b/specification/v2_storage.md @@ -0,0 +1,485 @@ +# V2 Storage Specification + +**Status:** Draft +**Issue:** [#983](https://github.com/modelcontextprotocol/inspector/issues/983) +**Last Updated:** 2026-01-19 + +## 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](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/1805), NOT browser localStorage + +--- + +## State Categories + +| 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 | + +### 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 (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_tech_stack.md](./v2_tech_stack.md#pino-rationale) for Pino configuration details. + +--- + +## 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 `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 +- Requires Provider at React root +- 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; response history | +| **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 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 + +### 1. Preferences Store + +**Purpose:** Persist user preferences across sessions + +**Persistence:** localStorage via `persist` middleware + +```typescript +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) + +```typescript +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) + +```typescript +interface ExecutionFormState { + // Tools + selectedToolName: string | null; + toolFormValues: Record; + toolResponseHistory: ToolResponse[]; + + // Resources + selectedResourceUri: string | null; + resourceContent: unknown | null; + + // Prompts + selectedPromptName: string | null; + promptFormValues: Record; + promptMessages: unknown[] | null; +} + +interface ToolResponse { + id: string; + toolName: string; + args: Record; + result: unknown; + timestamp: string; + duration: number; + isError: boolean; +} + +interface ExecutionFormActions { + selectTool: (name: string | null) => void; + setToolFormValues: (values: Record) => void; + addToolResponse: (response: ToolResponse) => void; + clearToolHistory: () => void; + + selectResource: (uri: string | null) => void; + setResourceContent: (content: unknown | null) => void; + + selectPrompt: (name: string | null) => void; + setPromptFormValues: (values: Record) => void; + setPromptMessages: (messages: unknown[] | null) => void; + + reset: () => void; +} +``` + +**History cap:** 50 tool responses + +### 4. Testing Profiles Store + +**Purpose:** Manage sampling/elicitation response configurations + +**Persistence:** localStorage via `persist` middleware + +```typescript +interface TestingProfilesState { + profiles: TestingProfile[]; + activeProfileId: string; +} + +interface TestingProfilesActions { + setActiveProfile: (id: string) => void; + addProfile: (profile: Omit) => TestingProfile; + updateProfile: (id: string, updates: Partial) => 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: + +```typescript +// client/src/stores/historyStore.ts +import type { HistoryRepository } from '@modelcontextprotocol/inspector-core'; + +export function createHistoryStore(repository: HistoryRepository) { + return create((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 - ConnectionService | | +| | - HistoryRepository - ExecutionService | | +| | - LogsRepository - TestingProfileRepository | | +| +------------------------+-------------------------------+ | ++---------------------------|-----------------------------------+ + | + v + +-------------------------------+ + | Core Package (React-free) | + | - MCP Client lifecycle | + | - Transport creation | + | - Handler interfaces | + | - Type definitions | + | - Memory implementations | + +-------------------------------+ +``` + +--- + +## 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 + +- [Issue #983](https://github.com/modelcontextprotocol/inspector/issues/983) - Data spec discussion +- [Discussion #1805](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/1805) - Server config storage decision +- [MCPJam Inspector](https://github.com/anthropics/mcpjam) - Reference implementation using Zustand +- [Zustand documentation](https://docs.pmnd.rs/zustand/getting-started/introduction) From 2570b7a5df61bac2b2b8e38f1a9cdab00a32c006 Mon Sep 17 00:00:00 2001 From: olaservo Date: Mon, 19 Jan 2026 07:25:54 -0700 Subject: [PATCH 2/4] Fix link to mcpjam --- specification/v2_storage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specification/v2_storage.md b/specification/v2_storage.md index 29ac6042a..37ad37208 100644 --- a/specification/v2_storage.md +++ b/specification/v2_storage.md @@ -481,5 +481,5 @@ client/src/stores/ - [Issue #983](https://github.com/modelcontextprotocol/inspector/issues/983) - Data spec discussion - [Discussion #1805](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/1805) - Server config storage decision -- [MCPJam Inspector](https://github.com/anthropics/mcpjam) - Reference implementation using Zustand +- [MCPJam Inspector](https://github.com/MCPJam/inspector) - Reference implementation using Zustand - [Zustand documentation](https://docs.pmnd.rs/zustand/getting-started/introduction) From 6a132d6d6148ff172c971537c4e63c562c81cabe Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 21 Jan 2026 08:14:38 -0700 Subject: [PATCH 3/4] docs(spec): update storage spec diagram to match implementation - Remove ConnectionService/ExecutionService from core interfaces (connection/execution state managed by React Context per spec text) - Remove Memory implementations from core package (repository interfaces kept as proxy API contracts) - Change "Handler interfaces" to "Handler functions" Co-Authored-By: Claude Opus 4.5 --- specification/v2_storage.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/specification/v2_storage.md b/specification/v2_storage.md index 37ad37208..e8421b0d4 100644 --- a/specification/v2_storage.md +++ b/specification/v2_storage.md @@ -411,8 +411,7 @@ This pattern: | v v | | +--------------------------------------------------------+ | | | Core Package Interfaces | | -| | - ServerConfigRepository - ConnectionService | | -| | - HistoryRepository - ExecutionService | | +| | - ServerConfigRepository - HistoryRepository | | | | - LogsRepository - TestingProfileRepository | | | +------------------------+-------------------------------+ | +---------------------------|-----------------------------------+ @@ -422,9 +421,8 @@ This pattern: | Core Package (React-free) | | - MCP Client lifecycle | | - Transport creation | - | - Handler interfaces | + | - Handler functions | | - Type definitions | - | - Memory implementations | +-------------------------------+ ``` From d755666e76800bfb5168f4debe8f37ac4a4b993f Mon Sep 17 00:00:00 2001 From: olaservo Date: Wed, 21 Jan 2026 08:42:57 -0700 Subject: [PATCH 4/4] fix(spec): correct Jotai provider claim - provider-less by default Jotai uses provider-less mode by default since v2. Provider is only needed for SSR, subtree isolation, or custom initial values. Co-Authored-By: Claude Opus 4.5 --- specification/v2_storage.md | 1 - 1 file changed, 1 deletion(-) diff --git a/specification/v2_storage.md b/specification/v2_storage.md index e8421b0d4..c6d16cad4 100644 --- a/specification/v2_storage.md +++ b/specification/v2_storage.md @@ -108,7 +108,6 @@ See [v2_tech_stack.md](./v2_tech_stack.md#pino-rationale) for Pino configuration **Cons:** - Less intuitive for imperative state updates -- Requires Provider at React root - Smaller ecosystem than Zustand **Verdict:** Good alternative, but less proven in MCP tooling context