Skip to content

Commit e9ac57c

Browse files
cliffhallclaude
andauthored
docs(spec): add v2 server-list file design (#1343) (#1344)
Design for replacing the hardcoded SEED_SERVERS in App.tsx with a file-backed list at ~/.mcp-inspector/mcp.json, reusing the v1.5-ported storage primitives and the canonical mcp.json parser. Ties the new doc into the Storage section nav as the first sub-menu item. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8d6dea0 commit e9ac57c

2 files changed

Lines changed: 195 additions & 0 deletions

File tree

specification/v2_servers_file.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Inspector V2 Tech Stack - Storage - Server List File
2+
3+
### Brief | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md)
4+
5+
#### [Web Client](v2_web_client.md) | [Server](v2_server.md) | Storage
6+
##### [Overview](v2_storage.md) | Server List File
7+
8+
## Summary
9+
10+
Replaces the hardcoded `SEED_SERVERS` in `clients/web/src/App.tsx:47` with a file-backed list at `~/.mcp-inspector/mcp.json`, read at startup, mutated via REST endpoints, surfaced through a `useServers` hook.
11+
12+
## Goals
13+
14+
- Persist the user's server list across restarts.
15+
- Use the canonical `{ mcpServers: { ... } }` format so the file is interoperable with Claude Desktop / Cursor / Cline and editable by hand.
16+
- Reuse the file-I/O facility already ported from v1.5 (`core/storage/store-io.ts`) and the parser already in `core/mcp/node/config.ts`.
17+
- Land full CRUD in one pass (per the scope decision) so the `onServerAdd` / `onServerEdit` / `onServerClone` / `onServerRemove` stubs in `App.tsx:639` stop lying.
18+
19+
## Non-goals
20+
21+
- Sync with Claude Desktop's `claude_desktop_config.json` location. We pick our own path; symlinking is the user's call.
22+
- Server schema validation beyond what `loadMcpServersConfig` already does (structural; no command-existence check).
23+
- Multi-user / multi-machine sync.
24+
- Migrating CLI/TUI to the default path — they already accept `--config <path>` via `core/mcp/node/config.ts`. The new default-path helper will be in core so they can adopt it later.
25+
26+
## File location
27+
28+
- **Path**: `~/.mcp-inspector/mcp.json` (Windows: `%USERPROFILE%\.mcp-inspector\mcp.json`).
29+
- **Why this dir**: `~/.mcp-inspector/storage/` already exists for the Zustand-persist stores (OAuth, settings); one Inspector dir under `$HOME` is friendlier than two. Resolution uses the same `process.env.HOME || process.env.USERPROFILE` fallback as `getDefaultStorageDir()` in `core/storage/store-io.ts:13`.
30+
- **Why canonical filename**: lets users symlink to/from Claude Desktop and similar tools.
31+
- **Permissions**: `0o600`, matching `writeStoreFile` in `core/storage/store-io.ts:55`.
32+
33+
## On-disk format
34+
35+
```jsonc
36+
{
37+
"mcpServers": {
38+
"filesystem-server-default": {
39+
"type": "stdio",
40+
"command": "npx",
41+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
42+
},
43+
"everything-server-default": {
44+
"type": "stdio",
45+
"command": "npx",
46+
"args": ["-y", "@modelcontextprotocol/server-everything"]
47+
}
48+
}
49+
}
50+
```
51+
52+
- Matches `MCPConfig` in `core/mcp/types.ts:68`.
53+
- `type` omitted → normalized to `"stdio"`; `type: "http"``"streamable-http"` (`normalizeServerType` in `core/mcp/node/config.ts:81`).
54+
- The map key is the **server `id`**. `ServerEntry.id` already documents itself this way (`core/mcp/types.ts:89`: "The MCPConfig.mcpServers map key").
55+
- Display name: derived from the map key. The Inspector adds **no extension fields** to keep `mcp.json` clean and tool-interoperable. The edit dialog therefore treats id and display name as the same field; renaming = key-rotate + carry config across.
56+
57+
## First-run behavior
58+
59+
If the file does not exist when the backend boots, write a file containing the two current `SEED_SERVERS`. User immediately sees a non-empty Servers screen and discovers the file by editing one of the seeds. Subsequent boots read whatever the user has saved.
60+
61+
## Architecture
62+
63+
### Reused
64+
65+
| Concern | File | What we reuse |
66+
|---|---|---|
67+
| Atomic R/W + ENOENT handling + 0o600 + `mkdir -p` | `core/storage/store-io.ts` | `readStoreFile`, `writeStoreFile`, `deleteStoreFile`, `parseStore`, `serializeStore` |
68+
| `mcp.json` parsing + type normalization | `core/mcp/node/config.ts` | `loadMcpServersConfig` (already used by the CLI/TUI runner code); `normalizeServerType` needs to be exported |
69+
| Hono backend + auth + storage routes pattern | `core/mcp/remote/node/server.ts` | `/api/storage/:storeId` is the template for the new `/api/servers` routes |
70+
| Auth'd fetch from browser | wired via `getAuthToken()` in `clients/web/src/App.tsx:84` | `useServers` will call the backend with `x-mcp-remote-auth: Bearer <token>` |
71+
72+
### Why not `createFileStorageAdapter` directly
73+
74+
`core/storage/adapters/file-storage.ts` is a Zustand `persist` adapter — it wraps the payload as `{ state, version }` so the file ends up looking like `{"state":{...},"version":0}`. That breaks the "human-editable canonical `mcp.json`" goal. We use the underlying `store-io.ts` primitives instead.
75+
76+
### New code
77+
78+
#### `core/storage/store-io.ts` (extend)
79+
80+
```ts
81+
export function getDefaultMcpConfigPath(): string {
82+
const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
83+
return path.join(homeDir, ".mcp-inspector", "mcp.json");
84+
}
85+
```
86+
87+
Co-located with `getDefaultStorageDir()` so the two path conventions stay in one file.
88+
89+
#### `core/mcp/serverList.ts` (new)
90+
91+
Pure converters between on-disk `MCPConfig` and in-memory `ServerEntry[]`. No I/O — easy to unit-test under happy-dom.
92+
93+
```ts
94+
export function mcpConfigToServerEntries(config: MCPConfig): ServerEntry[];
95+
export function serverEntriesToMcpConfig(entries: ServerEntry[]): MCPConfig;
96+
export function DEFAULT_SEED_CONFIG: MCPConfig; // the two existing seeds
97+
```
98+
99+
`mcpConfigToServerEntries` sets `connection: { status: "disconnected" }` and uses the map key as both `id` and `name`. `serverEntriesToMcpConfig` strips `connection` / `info` (runtime-only) before serializing.
100+
101+
Also re-export `normalizeServerType` from `core/mcp/node/config.ts` (or move it into `serverList.ts` and import it back into `config.ts`).
102+
103+
#### `core/mcp/remote/node/server.ts` (extend)
104+
105+
Add granular endpoints (mirror of `/api/storage/:storeId`, but specialized so the UI can do per-row mutations without read-modify-write across tabs):
106+
107+
| Method | Path | Body | Response |
108+
|---|---|---|---|
109+
| `GET` | `/api/servers` || `{ mcpServers: {...} }` — creates the file with seeds if absent |
110+
| `POST` | `/api/servers` | `{ id: string, config: MCPServerConfig }` | `{ ok: true }`; 409 if `id` already exists |
111+
| `PUT` | `/api/servers/:id` | `{ id?: string, config: MCPServerConfig }` | `{ ok: true }`; supports id rename (delete old key + write new) |
112+
| `DELETE` | `/api/servers/:id` || `{ ok: true }` (ignores missing) |
113+
114+
`id` is validated with `validateStoreId` (same alphanum+hyphen+underscore rule as store IDs — prevents anyone slipping `..` into the key). All routes serialize through the same atomic `writeStoreFile`, so concurrent writes are well-defined (last writer wins per-write; granularity reduces blast radius).
115+
116+
`RemoteServerOptions` gains:
117+
```ts
118+
/** Optional path for the user's server list file. Default: ~/.mcp-inspector/mcp.json */
119+
mcpConfigPath?: string;
120+
```
121+
122+
Defaulted via `getDefaultMcpConfigPath()`. `clients/web/server/vite-hono-plugin.ts:62` and `clients/web/server/server.ts:48` get a one-line update to pass `config.mcpConfigPath` if/when the web config grows the field; for v1, the default is sufficient.
123+
124+
#### `core/react/useServers.ts` (new)
125+
126+
```ts
127+
export interface UseServersResult {
128+
servers: ServerEntry[];
129+
loading: boolean;
130+
error: string | undefined;
131+
refresh: () => Promise<void>;
132+
addServer: (id: string, config: MCPServerConfig) => Promise<void>;
133+
updateServer: (originalId: string, newId: string, config: MCPServerConfig) => Promise<void>;
134+
removeServer: (id: string) => Promise<void>;
135+
}
136+
137+
export function useServers(opts: {
138+
baseUrl: string; // window.location.origin by default in callers
139+
authToken: string | undefined;
140+
}): UseServersResult;
141+
```
142+
143+
Fetches on mount via `fetch(`${baseUrl}/api/servers`, ...)` with the auth header. Holds the list in `useState`. Mutators do the HTTP call then re-fetch to keep server-and-client in sync (the list is small, ~tens of entries; optimistic merging is not worth the bug surface).
144+
145+
### `clients/web/src/App.tsx` changes
146+
147+
1. Remove the `SEED_SERVERS` constant (seeds move to `core/mcp/serverList.ts` as `DEFAULT_SEED_CONFIG`).
148+
2. Replace `const [servers] = useState<ServerEntry[]>(SEED_SERVERS);` with `const { servers, addServer, updateServer, removeServer } = useServers({ baseUrl: window.location.origin, authToken: getAuthToken() });`.
149+
3. Wire `onServerAdd` / `onServerEdit` / `onServerClone` / `onServerRemove` (currently `todoNoop` at `clients/web/src/App.tsx:639``646`) to the hook.
150+
4. Disconnect-on-remove: if the user removes the `activeServerId`, call `inspectorClient?.disconnect()` and clear active server state before the mutation. Matches the lifecycle the `useEffect` at `App.tsx:272` already enforces on unmount.
151+
5. Active-server pinning across rename: `updateServer` returns the new id; if `originalId === activeServerId`, update `activeServerId` to the new id.
152+
153+
### UI surfaces
154+
155+
The `InspectorView` prop interface already declares `onServerAdd` / `onServerEdit` / `onServerClone` / `onServerRemove` / `onServerImportConfig` / `onServerImportJson`. The dialogs themselves are TBD — out of scope for *this* spec is the visual design; in scope is wiring them to the new hook. If the Add/Edit dialog component does not exist yet, ship a minimal Mantine `Modal` + `TextInput` + transport-specific fields. Follow `clients/web/src/components/...` conventions (subcomponent constants via `.withProps()`, theme variants for styling — per `AGENTS.md`'s React rules).
156+
157+
`onServerImportConfig` / `onServerImportJson` map naturally to "paste a full `mcpServers` block" and "upload an `mcp.json` file"; both become bulk `POST /api/servers` calls in a loop (or a single `PUT /api/servers` that we add later). Defer until basic add/edit/remove is working.
158+
159+
## Test plan
160+
161+
Place tests per `AGENTS.md`'s integration-folder convention.
162+
163+
### Unit (`unit` vitest project, happy-dom)
164+
165+
- `clients/web/src/test/core/mcp/serverList.test.ts` — round-trip `MCPConfig``ServerEntry[]`; verifies the map key becomes the id, that `connection` / `info` are stripped on serialize, and that `normalizeServerType` is applied on parse.
166+
- `clients/web/src/test/core/storage/getDefaultMcpConfigPath.test.ts` — env-var permutations (`HOME` set / `USERPROFILE` set / neither).
167+
168+
### Integration (`integration` vitest project, node env, 30s)
169+
170+
- `clients/web/src/test/integration/mcp/remote/servers-route.test.ts` — spin up `createRemoteApp` with a tmp `mcpConfigPath`, exercise GET (file absent → seeds written; file present → returned), POST (success + 409 on dup), PUT (rename + payload update), DELETE (existing + missing). Mirrors the `adapters.test.ts` pattern already in `clients/web/src/test/integration/storage/adapters.test.ts`.
171+
- `clients/web/src/test/integration/react/useServers.test.tsx` — render the hook against a real `createRemoteApp` Hono instance (no mocking); assert load, add, update, remove flows reflect what the backend has on disk.
172+
173+
### Coverage
174+
175+
The 90% per-file gate applies to the new files. The pure converters and route handlers are easy; the React hook's error path needs explicit coverage (network error, 4xx response, 5xx response).
176+
177+
### Manual
178+
179+
Per `AGENTS.md`'s "test new or modified code" rule plus the UI-changes guidance: run `npm run dev`, verify (a) first launch writes the seeds, (b) editing the file by hand and reloading the browser shows the edit, (c) Add/Edit/Remove from the UI persist across a hard reload, (d) deleting the active server cleanly disconnects.
180+
181+
## Risks
182+
183+
- **Concurrent writes from multiple browser tabs.** Granular endpoints reduce the surface (per-row, not whole-file). Same-row contention is last-write-wins, which is fine for a config the user is editing manually. We do *not* add file locking; the cost outweighs the rare case.
184+
- **User edits the file while the browser is open.** Browser holds a stale list until the user hits Refresh on the Servers screen (the `refresh` returned by the hook). Acceptable for v1; auto-watching the file is a possible follow-up but `fs.watch` semantics across OSes are a long tail of bugs.
185+
- **Schema drift with Claude Desktop / Cursor.** They occasionally add fields (e.g. Claude Desktop's `disabled`). `loadMcpServersConfig` currently does `JSON.parse(...) as MCPConfig` — extra fields survive the round-trip as long as we don't filter them. The converters in `serverList.ts` should preserve unknown fields on `MCPServerConfig` rather than copying a fixed allow-list.
186+
- **Migration from `SEED_SERVERS`.** Existing dev users have no file. First boot writes one — they won't notice. No code path persists the in-memory `useState` list today, so nothing to migrate.
187+
188+
## Out of scope (follow-ups)
189+
190+
- Import-from-Claude-Desktop button (read `~/Library/Application Support/Claude/claude_desktop_config.json` or the Windows/Linux equivalent, merge into our file).
191+
- File watching for hot reload of external edits.
192+
- Per-server tags / folders / groups.
193+
- Export current list as JSON.
194+
- CLI/TUI: switch their default `--config` to `getDefaultMcpConfigPath()` when no `--config` flag is given. Touch when those clients are wired up to v2.

specification/v2_storage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Brief | [V1 Problems](v1_problems.md) | [V2 Scope](v2_scope.md) | V2 Tech Stack | [V2 UX](v2_ux.md)
44

55
#### [Web Client](v2_web_client.md) | [Server](v2_server.md) | Storage
6+
##### Overview | [Server List File](v2_servers_file.md)
67

78
## Overview
89

0 commit comments

Comments
 (0)