feat(web,core): persist & edit STDIO cwd/env in Server Settings (#1515) (#1518)

* feat(web,core): edit STDIO cwd/env in Server Settings (#1515)

Persist and edit a STDIO server's working directory and environment
variables through the Server Settings modal.

- Add `env` (pair-array, mirrors headers) and `cwd?` to
  `InspectorServerSettings`.
- `storedFieldsToInspectorSettings` lifts the stdio config's `env`/`cwd`
  into settings so the modal can show them; they stay on `config` for the
  transport. Add `envRecordToPairs` / `envPairsToRecord` helpers.
- `env`/`cwd` remain config-owned on disk (the standard mcp.json location):
  the `/api/servers` PUT route writes an edited settings.env/cwd back onto
  the stored stdio config on a settings-only patch (supporting clear), and a
  config-providing PUT keeps config authoritative. `validateSettings`
  accepts `env`/`cwd`.
- Server Settings modal: Working Directory field in Options and a dedicated
  collapsible Environment Variables section — both STDIO-only.
- Registry server.json import sets no `cwd` (the schema has no
  working-directory concept); documented in serverJson.ts.

Tests: serverList converter round-trip, route write-through (set/clear,
config-precedence, non-stdio no-leak, validation), import preservation
(client-config cwd, registry cwd skip), and form/modal component coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): preserve config env/cwd when a settings PUT omits them (#1515 review)

Address Claude's review of #1518.

- `/api/servers` PUT now distinguishes an *absent* `env`/`cwd` (preserve the
  stored value) from an explicit empty list / blank string (clear). Previously
  `validateSettings` coerced a missing `env` to `[]`, so an env-unaware caller
  patching only a timeout would silently wipe the server's env/cwd. The
  integrated web client always resends the full GET-rehydrated env, so it was
  unaffected — this protects the raw HTTP contract.
- `validateSettings` reports `envProvided`/`cwdProvided`; the write-through only
  touches `config.env`/`cwd` for fields the caller actually sent.
- On the omit path, rehydrate the untouched env's keychain values onto the
  rebuilt entry so the secret reconcile re-persists them rather than sweeping
  the blanked-on-disk keys out of the keychain.
- Minor: guard the stdio cast in `mcpConfigToServerEntries` on the transport
  type instead of casting unconditionally.
- Test: settings-only PUT omitting env/cwd preserves config keys + cwd + the
  keychain secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-06-24 19:15:43 -04:00 • committed by GitHub
commit 5ee347571f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -10,18 +10,30 @@ import {
type ServerSettingsSection,
} from "../ServerSettingsForm/ServerSettingsForm";
const ALL_SECTIONS: ServerSettingsSection[] = [
"options",
"headers",
"metadata",
"timeouts",
"oauth",
"roots",
];
// The "environment" section only renders for stdio servers, so it joins the
// expand/collapse-all set only then — otherwise `allExpanded` could never be
// reached (the section isn't in the DOM to expand).
function allSectionsFor(isStdio: boolean): ServerSettingsSection[] {
return [
"options",
...(isStdio ? (["environment"] as const) : []),
"headers",
"metadata",
"timeouts",
"oauth",
"roots",
];
}
export interface ServerSettingsModalProps {
opened: boolean;
settings: InspectorServerSettings;
/**
* Whether the target server uses the stdio transport. Forwarded to the form
* to gate the stdio-only Working Directory field and Environment Variables
* section.
*/
isStdio: boolean;
onClose: () => void;
onSettingsChange: (settings: InspectorServerSettings) => void;
}
@ -29,6 +41,7 @@ export interface ServerSettingsModalProps {
export function ServerSettingsModal({
opened,
settings,
isStdio,
onClose,
onSettingsChange,
}: ServerSettingsModalProps) {
@ -41,10 +54,11 @@ export function ServerSettingsModal({
ServerSettingsSection[]
>(["options"]);
const allExpanded = expandedSections.length === ALL_SECTIONS.length;
const allSections = allSectionsFor(isStdio);
const allExpanded = expandedSections.length === allSections.length;
function handleToggleAll() {
setExpandedSections(allExpanded ? [] : ALL_SECTIONS);
setExpandedSections(allExpanded ? [] : allSections);
}
function handleAddHeader() {
@ -68,6 +82,29 @@ export function ServerSettingsModal({
onSettingsChange({ ...settings, headers });
}
function handleAddEnv() {
onSettingsChange({
...settings,
env: [...settings.env, { key: "", value: "" }],
});
}
function handleRemoveEnv(index: number) {
onSettingsChange({
...settings,
env: settings.env.filter((_, i) => i !== index),
});
}
function handleEnvChange(index: number, key: string, value: string) {
const env = settings.env.map((e, i) => (i === index ? { key, value } : e));
onSettingsChange({ ...settings, env });
}
function handleCwdChange(value: string) {
onSettingsChange({ ...settings, cwd: value });
}
function handleAddMetadata() {
onSettingsChange({
...settings,
@ -158,11 +195,16 @@ export function ServerSettingsModal({
</Group>
<ServerSettingsForm
settings={settings}
isStdio={isStdio}
expandedSections={expandedSections}
onExpandedSectionsChange={setExpandedSections}
onAddHeader={handleAddHeader}
onRemoveHeader={handleRemoveHeader}
onHeaderChange={handleHeaderChange}
onAddEnv={handleAddEnv}
onRemoveEnv={handleRemoveEnv}
onEnvChange={handleEnvChange}
onCwdChange={handleCwdChange}
onAddMetadata={handleAddMetadata}
onRemoveMetadata={handleRemoveMetadata}
onMetadataChange={handleMetadataChange}