`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>
502 lines
18 KiB
Markdown
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)
|