ui: fix mcp panel for toggle + timeout + proxy + ON/OFF state (#25631)

* ui: fix MCP panel regressions after settings rework

Restore the llama-server proxy switch in the Add New Server dialog.
The dialog never passed useProxy/onUseProxyChange to McpServerForm,
which only renders the proxy switch when the handler is provided.
The flag is now wired, persisted on addServer, and reset on close.

Bound the MCP connection handshake with the configured timeout.
handshakeTimeoutMs was set in the server config but never consumed.
The SDK timeout only covers the initialize request, not
transport.start(), which can hang forever on an unreachable host.
The whole handshake now races against the timeout and closes the
transport on expiry so the underlying fetch or socket is aborted.

Keep disabled MCP servers visible in management and chat-add UIs.
Collapsing mcpDefaultServerOverrides into mcpServers[i].enabled turned
the visibleMcpServers enabled filter into a visibility trap: toggling
a server off outside a conversation hid it from every surface with no
way to re-enable it. The filter is dropped, tools derived from health
checks still skip disabled servers, and the settings page and server
card render the real card instead of a skeleton for disabled servers
that never receive a startup health check.

* ui: clarify MCP server list semantics and add regression test

Remove the visibleMcpServers getter, a filterless alias of getServers
whose name invites the next refactor to put a filter back. Call sites
read getServers directly, the duplicate list in the chat submenu is
merged, and the misleading local variable in the sheet is renamed.

A parser unit test pins the invariant: enabled is an on/off state,
never a visibility filter, so disabled servers stay listed and
toggleable.

* ui: apply the MCP request timeout setting live to all servers

The per-server requestTimeoutSeconds field was never editable in any
UI and froze the global setting at server creation time, so changing
the timeout in Settings was a no-op for existing servers. The field
is removed from the data model and parsers, the timeout is read live
from the global setting wherever a request config is built, and the
misleading "Can be overridden per server" help text is dropped. A
parser unit test guards against reintroducing the stored field.

* ui: move the MCP request timeout into the Agentic settings section

The MCP section held a single setting. The timeout is a global tool
execution parameter like the other Agentic entries, so it moves there
and the section is removed. Same settings key, no migration needed.

* ui: remove the dead tool preview lines setting

The agenticMaxToolPreviewLines setting was read into AgenticConfig
and consumed by nothing: the agentic loop only uses enabled and
maxTurns. Its help text described a previous architecture where only
truncated previews and the final response survived the loop; tool
results and intermediate turns now persist as full DB messages, so
the setting had no effect at any value. Stale keys in localStorage
or a server ui-config are ignored.

* ui: resolve absent MCP per-chat overrides to the server enabled flag

New conversations started with every MCP server off: the settings
rework stopped seeding a per-conversation override list, assuming
the enabled check would fall back to mcpServers[i].enabled, but it
fell back to false, and the send path passed the raw stored list
with no fallback at all. The per-conversation list is now sparse by
contract, holding only explicit toggles, and every access point
resolves a missing entry to the server's own enabled flag: the
toggle display, the resolved list handed to the agentic flow, and
the enabled check itself.
This commit is contained in:
Pascal
2026-07-14 16:50:44 +02:00
committed by GitHub
parent 7f575c39d6
commit 17a05e451f
19 changed files with 126 additions and 127 deletions
@@ -17,10 +17,10 @@
let { onMcpSettingsClick }: Props = $props();
let mcpSearchQuery = $state('');
let allMcpServers = $derived(mcpStore.getServers());
let mcpServers = $derived(mcpStore.visibleMcpServers);
// Every configured server is listed; `enabled` is an on/off state,
// not a visibility filter, so a disabled server stays toggleable.
let mcpServers = $derived(mcpStore.getServers());
let hasMcpServers = $derived(mcpServers.length > 0);
// let hasAnyMcpServers = $derived(allMcpServers.length > 0);
let filteredMcpServers = $derived.by(() => {
const query = mcpSearchQuery.toLowerCase().trim();
if (!query) return mcpServers;
@@ -46,7 +46,7 @@
function handleMcpSubMenuOpen(open: boolean) {
if (open) {
mcpSearchQuery = '';
mcpStore.runHealthChecksForServers(allMcpServers);
mcpStore.runHealthChecksForServers(mcpServers);
}
}
@@ -84,7 +84,7 @@
const sheetItemRowClass =
'flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent';
let visibleMcpServers = $derived(mcpStore.visibleMcpServers);
let mcpServers = $derived(mcpStore.getServers());
</script>
<div class="flex items-center gap-1 {className}">
@@ -218,13 +218,13 @@
<span class="flex-1">MCP Servers</span>
<span class="text-xs text-muted-foreground">
{visibleMcpServers.length} server{visibleMcpServers.length !== 1 ? 's' : ''}
{mcpServers.length} server{mcpServers.length !== 1 ? 's' : ''}
</span>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="flex flex-col gap-0.5 pl-4">
{#each visibleMcpServers as server (server.id)}
{#each mcpServers as server (server.id)}
{@const healthState = mcpStore.getHealthCheckState(server.id)}
{@const hasError = healthState.status === HealthCheckStatus.ERROR}
{@const displayName = mcpStore.getServerLabel(server)}
@@ -267,7 +267,7 @@
</button>
{/each}
{#if visibleMcpServers.length === 0}
{#if mcpServers.length === 0}
<div class="px-3 py-2 text-center text-sm text-muted-foreground">
No MCP servers configured
</div>
@@ -16,6 +16,7 @@
let newServerUrl = $state('');
let newServerHeaders = $state('');
let newServerUseProxy = $state(false);
let newServerUrlError = $derived.by(() => {
if (!newServerUrl.trim()) return 'URL is required';
try {
@@ -35,6 +36,7 @@
if (!value) {
newServerUrl = '';
newServerHeaders = '';
newServerUseProxy = false;
}
open = value;
onOpenChange?.(value);
@@ -49,7 +51,8 @@
id: newServerId,
enabled: true,
url: newServerUrl.trim(),
headers: newServerHeaders.trim() || undefined
headers: newServerHeaders.trim() || undefined,
useProxy: newServerUseProxy
});
conversationsStore.setMcpServerOverride(newServerId, true);
@@ -74,8 +77,10 @@
<McpServerForm
url={newServerUrl}
headers={newServerHeaders}
useProxy={newServerUseProxy}
onUrlChange={(v) => (newServerUrl = v)}
onHeadersChange={(v) => (newServerHeaders = v)}
onUseProxyChange={(v) => (newServerUseProxy = v)}
urlError={newServerUrl ? newServerUrlError : null}
id="new-server"
/>
@@ -32,7 +32,9 @@
let isHealthChecking = $derived(healthState.status === HealthCheckStatus.CONNECTING);
let isConnected = $derived(healthState.status === HealthCheckStatus.SUCCESS);
let isError = $derived(healthState.status === HealthCheckStatus.ERROR);
let showSkeleton = $derived(isIdle || isHealthChecking);
// Disabled servers stay IDLE (no startup health check), so the body
// skeleton only applies while a check is running or expected to run.
let showSkeleton = $derived(isHealthChecking || (isIdle && server.enabled));
let errorMessage = $derived(
healthState.status === HealthCheckStatus.ERROR ? healthState.message : undefined
);
@@ -22,7 +22,9 @@
let { class: className }: Props = $props();
let servers = $derived(mcpStore.visibleMcpServers);
// Every configured server is listed; `enabled` is an on/off state,
// not a visibility filter, so a disabled server stays toggleable.
let servers = $derived(mcpStore.getServers());
let isAddingServer = $state(false);
@@ -58,9 +60,14 @@
// Each card decides for itself whether to render based on its own
// health-check state, so adding a server only flashes the new card
// (not every other already-loaded card) until its health check resolves.
function isServerPending(serverId: string): boolean {
// Disabled servers never receive a startup health check, so IDLE only
// counts as pending when the server is enabled; otherwise the real card
// renders and keeps the enable toggle reachable.
function isServerPending(serverId: string, enabled: boolean): boolean {
const status = mcpStore.getHealthCheckState(serverId).status;
return status === HealthCheckStatus.IDLE || status === HealthCheckStatus.CONNECTING;
return (
status === HealthCheckStatus.CONNECTING || (status === HealthCheckStatus.IDLE && enabled)
);
}
</script>
@@ -109,7 +116,7 @@
style="grid-template-columns: repeat(auto-fill, minmax(min(32rem, calc(100dvw - 2rem)), 1fr));"
>
{#each servers as server (server.id)}
{#if isServerPending(server.id)}
{#if isServerPending(server.id, server.enabled)}
<McpServerCardSkeleton />
{:else}
<McpServerCard