revert(web): drop custom headers from the manual add form (#1915)

Server Settings already has a Custom Headers editor, so adding one to the
manual add form created a second place to edit the same field. The reporter
had also conditionally withdrawn #1915 — "editing that information from the
server settings dialog is sufficient" once #1848 closed, which it has.

Reverts the add-form editor, the shared KeyValueRows element, the optional
`settings` argument on addServer / updateServer, and the App-level merge
helper. What remains are two defects found while working in
ServerSettingsForm, which this PR now closes instead (#2040): the Custom
Headers hint misstating OAuth precedence, and the key/value rows' missing
accessible names.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-16 22:57:34 -04:00
commit 4dd8ed4498

View file

@ -13,7 +13,6 @@ import {
TextInput,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import { KeyValueRows } from "../../elements/KeyValueRows/KeyValueRows";
import type { ProtocolEra } from "@modelcontextprotocol/client";
import type {
InspectorServerSettings,
@ -224,6 +223,84 @@ const ClearStoredOAuthHint = Text.withProps({
miw: "12rem",
});
function KeyValueRows({
items,
entityLabel,
onChange,
onRemove,
}: {
items: { key: string; value: string }[];
/**
* Singular noun for one row ("header", "environment variable", …). Used only
* to build each control's `aria-label`: the section heading and the "Key" /
* "Value" placeholders are not programmatically associated with the inputs,
* so without it an assistive technology cannot tell one section's key box
* from another's, and every remove button announces only "X". The row number
* rides along because two rows can carry the same key — mid-edit, or a
* duplicate a server persisted — and a key-only name would leave both rows'
* controls indistinguishable, which is the thing this exists to prevent.
*/
entityLabel: string;
onChange: (index: number, key: string, value: string) => void;
onRemove: (index: number) => void;
}) {
/* v8 ignore next 3 -- unreachable: every caller guards with `length === 0`
and renders an EmptyHint instead, so KeyValueRows is only mounted with
a non-empty list. */
if (items.length === 0) {
return null;
}
return (
<>
{items.map((item, index) => {
const key = item.key.trim();
const rowLabel = key ? `${key}, row ${index + 1}` : `row ${index + 1}`;
return (
<Group key={index} grow>
<ClearableTextInput
placeholder="Key"
aria-label={`${entityLabel} name, ${rowLabel}`}
value={item.key}
onChange={(e) =>
onChange(index, e.currentTarget.value, item.value)
}
rightSection={
item.key ? (
<ClearButton
aria-label={`Clear ${entityLabel} name, ${rowLabel}`}
onClick={() => onChange(index, "", item.value)}
/>
) : null
}
/>
<ClearableTextInput
placeholder="Value"
aria-label={`${entityLabel} value, ${rowLabel}`}
value={item.value}
onChange={(e) => onChange(index, item.key, e.currentTarget.value)}
rightSection={
item.value ? (
<ClearButton
aria-label={`Clear ${entityLabel} value, ${rowLabel}`}
onClick={() => onChange(index, item.key, "")}
/>
) : null
}
/>
<RemoveIcon
aria-label={`Remove ${entityLabel}, ${rowLabel}`}
onClick={() => onRemove(index)}
>
X
</RemoveIcon>
</Group>
);
})}
</>
);
}
// Reserved-key rejection is inline per row (#2018): the reason is passed as the
// key input's `error` **string**, so Mantine renders it in the input's own error
// slot and wires the `aria-describedby` association — a bare boolean would mark