feat(servers): flatten mcp.json settings to top-level fields (#1358)#1359
Conversation
Eliminate the nested `settings` block introduced in #1352 so each server entry's Inspector-extension fields live as direct keys alongside `type` / `url`, matching the `.mcp.json` shape Claude Code / Cursor / Cline write: - `headers` becomes a `Record<string, string>` on disk (was nested `settings.headers` as a pair-array) - `oauth` becomes a nested `{ clientId?, clientSecret?, scopes? }` object (was flat `settings.oauthClientId` / `oauthClientSecret` / `oauthScopes`) - `metadata`, `connectionTimeout`, `requestTimeout` move to the entry's top level as-is (Inspector-only fields) The in-memory + wire shape is unchanged from #1352: `InspectorServerSettings` keeps pair-array `headers` and flat `oauth*` fields because the form needs them in that shape to drive controlled-component editing. The bidirectional conversion lives in `serverList.ts` (`storedFieldsToInspectorSettings` / `inspectorSettingsToStoredFields`) and is invoked by `mcpConfigToServerEntries` / `serverEntriesToMcpConfig` and by the server route's `buildStoredEntry`. The `PUT /api/servers/:id` wire envelope keeps the preserve/clear/apply semantics from #1353 — the backend splats validated `settings.*` onto top-level disk keys when assembling the next on-disk shape. Hard-cutover migration per the issue's decision 4: files written by the one pre-#1358 v2/main build have a nested `settings` block; we drop the node on read with a one-line warn including the server id. The persisted headers / metadata / timeouts / OAuth credentials are intentionally lost on first read — users re-enter them via the form (or hand-edit the file into the flat shape). v2 has not shipped a stable release with the nested shape, so the blast radius is dogfooders only. The smuggle-guard in `buildStoredEntry` widens from just `settings` to all six Inspector-extension keys (`settings`, `headers`, `metadata`, `connectionTimeout`, `requestTimeout`, `oauth`), so a caller can't bypass `validateSettings` by nesting any of them inside `config`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @cliffhall's task in 2m 43s —— View job Code review — feat(servers): flatten mcp.json settings to top-level fields
OverallClean execution of Approach C. The invariant "disk shape flattens; wire/memory/form unchanged" is held cleanly by localizing the conversion to two named helpers ( Observations1. [Low · maintainability] Three parallel enumerations of the extension keys, with no compile-time coupling. The five flat keys (
Suggestion: either derive the two runtime enumerations from 2. [Low · defense] Read-path has no shape validation for Inspector-extension fields.
Not a bug (lenient-on-read is consistent with 3. [Very low · nit] Warning visibility in dev without a logger.
4. [Very low · doc rot] Stale reference in the spec.
5. [Low · UX] Hard cutover on legacy files is irrecoverable by accident. The PR description (and Nice-to-have: Nits
Strengths
|
- Single-source the Inspector-extension key list with `satisfies` (finding 1). `INSPECTOR_FIELD_KEY_MAP satisfies Record<keyof StoredInspectorFields, true>` in serverList.ts means a new field on the type forces a compile error here. The set is exported and re-used by the server route's smuggle guard, so adding `proxy` or similar in the future updates all three sites in lockstep. The PUT preserve path now uses a new `stripInspectorFields` helper that derives from the same source instead of hand-destructuring. - Add read-path shape validation in `normalizeMcpServers` (finding 2): `headers: "oops"` / `metadata: 42` / `oauth: []` / non-numeric timeouts are dropped individually with a warn, so a hand-edited malformed file can't put garbage rows into the form. Mirrors the write-side `validateSettings` symmetry. New integration test covers each malformed shape with a partial fixture so a single bad key doesn't take out the whole entry. - Align new warns to the `fileLogger || console.warn` fallback pattern (finding 3) via a small `logWarn` helper, so the legacy-drop and smuggle warns are visible in `npm run dev` where the logger is often unconfigured. - Spec doc tweak: the CLI/TUI out-of-scope note's "writes to `settings.headers`" reference now reads "writes to the entry's top-level `headers` field on disk (post-#1358 flat shape)" (finding 4). Avoids confusion for future readers. - Nits: replace `current.mcpServers[originalId]!` with explicit narrowing; add a comment on the OAuth truthiness drop in `storedFieldsToInspectorSettings` mirroring the write-side comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the review. Five findings + two nits, all addressed in 3ce3420 except finding 5 which I'm leaving deliberate (rationale below). 1. Three parallel enumerations → single source via 2. Read-path shape validation — addressed in 3. Warn visibility in dev — addressed. New 4. Stale spec reference — addressed. The CLI/TUI out-of-scope line ( 5. Hard-cutover sidecar for legacy Nits — both addressed:
All checks green:
|
|
@claude review |
|
Claude finished @cliffhall's task in 2m 11s —— View job Follow-up review — fix commit
|
- Tighten `isNonNegNumber` with `Number.isFinite` so `Infinity` and `NaN` are rejected at the read boundary, matching the write-side intent (finding D). Hand-edited `connectionTimeout: Infinity` would have passed the prior check and propagated to the form. - Add a one-line comment in `normalizeMcpServers` noting that the per-entry object is mutated in place via the `delete` calls — safe for today's callers (freshly parsed JSON, no other reference held) but worth flagging for the next reader (finding A). Skipping findings B (move type guards to module scope — single call site) and C (`isOauthObject` lenient on extra keys — reviewer confirmed this is the intended forward-compat behavior, matching `normalizeServerType`'s unknown-key passthrough). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the second-pass approval. Picked up the two of four new observations that warranted code changes — f94cd85: A — in-place mutation comment: added a one-line comment on D — B — type guards at module scope: skipped. They're closing over nothing and would move cleanly, but a single call site doesn't justify the file split. If a future surface needs them, that's the moment to lift. C — Also took note of the suggestion to document the Validate still green: 142 files / 1690 tests / 306 storybook, coverage gate satisfied. |
|
@claude review |
|
Claude finished @cliffhall's task in 1m 15s —— View job Third-pass review — fix commit
|
The previous comment named the GET handler's "seed branch" as a caller of normalizeMcpServers, but that branch returns DEFAULT_SEED_CONFIG directly without normalizing. The actual second caller is the file-present branch (after parseStore). Correcting the label so a future reader looking for the seed branch caller doesn't waste time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Good catch — the seed branch returns Only a comment change, no test impact. |
Summary
headers,metadata,connectionTimeout,requestTimeout,oauth) live as direct keys alongsidetype/url, matching the.mcp.jsonshape Claude Code / Cursor / Cline write — hand-editedheaders: { ... }from those tools' docs is now readable on first connect.headerson disk becomes aRecord<string, string>(per Eliminate the mcp.json settings node and promote its fields to the top level #1358 decision 1);oauthbecomes a nested{ clientId?, clientSecret?, scopes? }object (decision 2). Inspector-onlymetadata/ timeouts flatten as-is (decision 3).InspectorServerSettingskeeps pair-arrayheadersand flatoauth*fields because the form needs them in that shape for controlled-component editing. Conversion at the persistence boundary lives inserverList.ts(storedFieldsToInspectorSettings/inspectorSettingsToStoredFields).{ id, config, settings }onPUT /api/servers/:idpreserved per decision 5; the preserve/clear/apply semantics from feat(servers): persist per-server settings to mcp.json (#1352) #1353 are intact end-to-end.normalizeMcpServersdrops a pre-Eliminate the mcp.json settings node and promote its fields to the top level #1358 nestedsettingsblock on read with a one-line warn; persisted headers / metadata / timeouts / OAuth credentials are intentionally lost.Closes #1358.
Approach
The implementation took Approach C from the design discussion: only the on-disk shape changes; the wire + memory + form shapes all stay as today. This achieves the issue's stated motivation (interop with hand-edited
.mcp.jsonfiles compatible with Claude / Cursor / Cline ) with the smallest diff — the form / modal / App.tsx / InspectorClient / transport plumbing is all untouched. The PR description in #1358 originally hinted at also flattening the wire shape (Approach A), but the user-facing benefit is identical and Approach C avoids a stateful-form refactor.Files changed
core/mcp/types.ts—StoredMCPServerflattens;InspectorServerSettingsunchanged.core/mcp/serverList.ts— newstoredFieldsToInspectorSettings/inspectorSettingsToStoredFieldshelpers, plus updatedmcpConfigToServerEntries/serverEntriesToMcpConfig.core/mcp/remote/node/server.ts—normalizeMcpServersdrops legacysettingswith warn;buildStoredEntrysplats settings.* into top-level disk keys; smuggle guard widens to all six Inspector-extension keys; PUT preserve/clear paths read flat fields offexisting.specification/v2_servers_file.md— example + per-server-settings section rewritten for the flat shape.Test plan
Acceptance criteria
🤖 Generated with Claude Code