ui: fix MCP server display name conflicts in tools lists (#26011)

* ui: fix MCP server display name conflicts in tools lists

Tool groups were keyed by display label so two servers reporting
the same name broke the keyed each blocks and only one was visible.
Key rendering, expand state and toggles by the stable server id
instead, and suffix duplicate labels with a counter in config order.

* ui: customizable MCP server display name with autofill

Add a display name field to the MCP server form, add and edit alike.
The custom name takes precedence over the server-reported one, so two
servers reporting the same name can be told apart; clearing the field
returns to the automatic label. In the add dialog a debounced preview
handshake prefills the field with the server-reported name: a manual
edit freezes the autofill, stale responses are discarded, failures
stay silent, and an unedited prefill is not persisted so the label
keeps following the server.

* ui: fix recursive fetch passthrough in the client test setup

The original fetch was captured inside beforeEach, where it is the
previous test's spy since vi.spyOn returns the existing one, so the
default passthrough recursed on itself for any URL outside the
mocked set. Capture the real fetch once at module load.
This commit is contained in:
Pascal
2026-07-24 19:28:14 +02:00
committed by GitHub
parent 27209a598d
commit 95a923a64c
15 changed files with 257 additions and 35 deletions
@@ -15,6 +15,7 @@
REDACTED_HEADERS
} from '$lib/constants';
import { browser } from '$app/environment';
import { HealthCheckStatus } from '$lib/enums';
interface Props {
open: boolean;
@@ -24,6 +25,16 @@
let { open = $bindable(), onOpenChange }: Props = $props();
let newServerUrl = $state('');
let newServerName = $state('');
let nameAutoFilled = $state('');
let nameTouched = $state(false);
let previewRun = 0;
function handleNameChange(value: string) {
newServerName = value;
nameTouched = true;
}
let newServerHeaders = $state('');
let newServerUseProxy = $state(false);
@@ -115,6 +126,48 @@
}
});
// Debounced preview handshake: once the URL is valid and stable, fetch the
// server-reported name to prefill the display name field. A manual edit
// freezes the autofill for good, and failures stay silent.
$effect(() => {
const url = newServerUrl.trim();
const headers = newServerHeaders.trim();
const useProxy = newServerUseProxy;
if (!open || newServerUrlError || !url) return;
const run = ++previewRun;
// One throwaway id per run: concurrent previews (URL typed, then the
// bearer token pasted) would poison each other's shared health state.
const previewId = `${MCP_SERVER_ID_PREFIX}-preview-${run}`;
const timer = setTimeout(async () => {
await mcpStore.runHealthCheck({
id: previewId,
enabled: false,
url,
headers: headers || undefined,
useProxy
});
const state = mcpStore.getHealthCheckState(previewId);
mcpStore.clearHealthCheck(previewId);
if (run !== previewRun) return;
if (state.status !== HealthCheckStatus.SUCCESS) return;
const autoName = state.serverInfo?.title || state.serverInfo?.name || '';
if (autoName && !nameTouched) {
newServerName = autoName;
nameAutoFilled = autoName;
}
}, 600);
return () => clearTimeout(timer);
});
let hasSelection = $derived(selectedRecommendationId !== null);
let unconfiguredRecommendations = $derived.by(() => {
@@ -146,6 +199,10 @@
function handleOpenChange(value: boolean) {
if (!value) {
newServerUrl = '';
newServerName = '';
nameAutoFilled = '';
nameTouched = false;
previewRun++;
newServerHeaders = '';
newServerUseProxy = false;
newServerWantsAuthorization = false;
@@ -163,6 +220,12 @@
id: newServerId,
enabled: true,
url: newServerUrl.trim(),
// A name equal to the autofilled server-reported one is not a
// customization: keep following the automatic label.
displayName:
newServerName.trim() && newServerName.trim() !== nameAutoFilled.trim()
? newServerName.trim()
: undefined,
headers: newServerHeaders.trim() || undefined,
useProxy: newServerUseProxy
});
@@ -210,6 +273,8 @@
<div class="space-y-4 py-4">
<McpServerForm
url={newServerUrl}
name={newServerName}
onNameChange={handleNameChange}
headers={newServerHeaders}
useProxy={newServerUseProxy}
onUrlChange={(v) => (newServerUrl = v)}