inspector/specification/v2_storage.md
cliffhall 3b995f6658 docs(spec): describe tool selection by row key, not name
`v2_ux_interfaces.md` and `v2_storage.md` still prescribed `selectedName` /
`selectedToolName` and `tools.find(t => t.name === …)` — the exact shape that
made a duplicated tool name uninspectable. Point them at `toolRowKey` /
`findToolByRowKey` and note the split between UI identity (the row key,
carried by `onSelectTool`) and wire identity (the protocol name, carried by
`onCallTool`).

Follow-up to #2001 review feedback.

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-17 00:23:26 -04:00

502 lines
18 KiB
Markdown

# Inspector V2 Tech Stack - Storage Specification
### [Brief](README.md) | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md) | [V2 Auth](v2_auth.md) | [V2 New Spec Impact](v2_new_spec_impact.md)
#### [Web Client](v2_web_client.md) | [CLI, TUI, Launcher](v2_cli_tui_launcher.md) | [Server](v2_server.md) | Storage
##### Overview | [Server List File](v2_servers_file.md)
## 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 |
|----------|----------|-------------|----------|
| **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](./v2_server.md#pino-rationale) 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](v2_auth_ema.md#oauth-persistence-1549--done), [Mid-session auth](v2_auth_mid_session.md).
---
## 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
- 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 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](https://docs.pmnd.rs/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](#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
```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
// The selected row's `toolRowKey` (`index:name`), not the tool's name — a
// `tools/list` may repeat a name (#2001).
selectedToolKey: 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
```typescript
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:
```typescript
// 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
- [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/MCPJam/inspector) - Reference implementation using Zustand
- [Zustand documentation](https://docs.pmnd.rs/zustand/getting-started/introduction)