fix: promote MCP server display name to a required form field (#24652)

A customer reported that on the `/agents` MCP server create form, the
Create button stays disabled even after filling in Slug and Server URL.
The form also requires a non-empty display name, but the display name
was rendered as a placeholder-style inline title, so it looked optional.

Addressing [Tracy's
feedback](https://github.com/coder/coder/pull/24652#issuecomment-4301473164),
this PR promotes the display name to a proper labeled form field,
matching Slug and Server URL.

## Changes

- Remove the inline editable title input and pencil icon from the
header.
- Header now shows a static server name (or `New MCP server` when
creating).
- Add a labeled `Display name` field as the first field in the form,
with the same required marker treatment used by Slug and Server URL.
- Update the `CreateServer` story to match (no longer asserts the
removed pencil icon).

## Screenshot

![MCP server create form with Display name as a proper labeled field
above
Slug](https://raw.githubusercontent.com/david-fraley/coder/pr-24652-screenshots/mcp-server-required-display-name.png)

Reported via Support for a Netflix user on the MCP server admin panel.

---

_Filed on behalf of the user by Coder Agents._

---------

Co-authored-by: Jaayden Halko <jaayden@coder.com>
This commit is contained in:
david-fraley
2026-04-23 14:51:25 +01:00
committed by GitHub
co-authored by Jaayden Halko
parent 1e21b288b9
commit f96f7b992f
2 changed files with 30 additions and 40 deletions
@@ -162,21 +162,18 @@ export const CreateServer: Story = {
await body.findByRole("button", { name: /Add your first server/i }),
);
// Fill in the display name via the inline header input.
// Fill in the Display name field.
const nameInput = await body.findByLabelText(/Display Name/i);
await userEvent.type(nameInput, "Sentry");
// Required fields (slug, server URL) are always visible. Optional
// sections start collapsed and the Enabled switch is edit-only.
// PencilIcon sits next to the editable name.
// Required fields (display name, slug, server URL) are always visible.
// Optional sections start collapsed and the Enabled switch is edit-only.
expect(body.getByLabelText(/^Slug/i)).toBeInTheDocument();
expect(body.getByLabelText(/Server URL/i)).toBeInTheDocument();
expect(body.queryByLabelText(/Description/i)).not.toBeInTheDocument();
expect(
body.queryByRole("switch", { name: /Enabled/i }),
).not.toBeInTheDocument();
const nameRow = nameInput.closest("div")?.parentElement;
expect(nameRow?.querySelector("svg.lucide-pencil")).toBeTruthy();
// Slug should auto-populate from the display name.
await expect(body.getByLabelText(/^Slug/i)).toHaveValue("sentry");
@@ -4,7 +4,6 @@ import {
ChevronDownIcon,
ChevronRightIcon,
CircleIcon,
PencilIcon,
PlusIcon,
ServerIcon,
XIcon,
@@ -471,41 +470,15 @@ const ServerForm: FC<ServerFormProps> = ({
return (
<div className="flex min-h-full flex-col">
{/* Back */}
<BackButton onClick={onBack} />
{/* Header with icon + editable name + enabled toggle */}
<div className="flex items-center gap-3">
<MCPServerIcon
iconUrl={form.values.iconURL}
name={form.values.displayName || "New server"}
className="h-8 w-8"
/>
<div className="inline-flex items-center gap-1">
<div className="relative inline-grid">
<span
className="invisible col-start-1 row-start-1 whitespace-pre text-lg font-medium"
aria-hidden="true"
>
{form.values.displayName || "Server display name"}
</span>
<input
type="text"
value={form.values.displayName}
onChange={(e) => {
form.setFieldValue("displayName", e.target.value);
if (!form.values.slugTouched) {
form.setFieldValue("slug", slugify(e.target.value));
}
}}
disabled={isDisabled}
spellCheck={false}
className="col-start-1 row-start-1 m-0 min-w-0 border-0 bg-transparent p-0 text-lg font-medium text-content-primary outline-none placeholder:text-content-secondary focus:ring-0"
placeholder="Server display name"
aria-label="Display Name"
/>
</div>
<PencilIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</div>
<span
className="truncate text-lg font-medium text-content-primary"
title={form.values.displayName || undefined}
>
{form.values.displayName ||
(isEditing ? "Server display name" : "New MCP server")}
</span>
{isEditing && (
<Tooltip>
<TooltipTrigger asChild>
@@ -538,6 +511,26 @@ const ServerForm: FC<ServerFormProps> = ({
>
<div className="space-y-6">
<div className="space-y-4">
<Field
label="Display name"
htmlFor={`${formId}-display-name`}
required
>
<Input
id={`${formId}-display-name`}
className="h-9 text-[13px]"
value={form.values.displayName}
onChange={(e) => {
form.setFieldValue("displayName", e.target.value);
if (!form.values.slugTouched) {
form.setFieldValue("slug", slugify(e.target.value));
}
}}
placeholder="e.g. Sentry"
disabled={isDisabled}
aria-label="Display Name"
/>
</Field>
<Field label="Slug" htmlFor={`${formId}-slug`} required>
<Input
id={`${formId}-slug`}