From 0253fb21f595246f54c192fe8332f34173be251b Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Sat, 16 May 2026 15:20:27 +0200 Subject: [PATCH] ui: Add request timeout for MCP tool calls (#23138) * feat: Add request timeout for MCP tool calls in llama-ui * feat: MCP Settings tab with max timeout setting --- .../SettingsChat/SettingsChatFields.svelte | 2 ++ tools/ui/src/lib/constants/routes.ts | 1 + tools/ui/src/lib/constants/settings-keys.ts | 1 + .../ui/src/lib/constants/settings-registry.ts | 21 ++++++++++++++++++- tools/ui/src/lib/services/mcp.service.ts | 10 ++++++--- tools/ui/src/lib/stores/mcp.svelte.ts | 7 +++++-- tools/ui/src/lib/types/mcp.d.ts | 2 ++ tools/ui/src/lib/types/settings.d.ts | 1 + tools/ui/src/lib/utils/mcp.ts | 6 ++++-- 9 files changed, 43 insertions(+), 8 deletions(-) diff --git a/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte b/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte index 3ecf00adc..069855eeb 100644 --- a/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte +++ b/tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte @@ -79,6 +79,8 @@
{ // Update local config immediately for real-time badge feedback diff --git a/tools/ui/src/lib/constants/routes.ts b/tools/ui/src/lib/constants/routes.ts index 14416478f..3b3fceea4 100644 --- a/tools/ui/src/lib/constants/routes.ts +++ b/tools/ui/src/lib/constants/routes.ts @@ -8,6 +8,7 @@ export const SETTINGS_SECTION_SLUGS = { PENALTIES: 'penalties', AGENTIC: 'agentic', DEVELOPER: 'developer', + MCP: 'mcp', TOOLS: 'tools', IMPORT_EXPORT: 'import-export' } as const; diff --git a/tools/ui/src/lib/constants/settings-keys.ts b/tools/ui/src/lib/constants/settings-keys.ts index b673bff27..92a57f88a 100644 --- a/tools/ui/src/lib/constants/settings-keys.ts +++ b/tools/ui/src/lib/constants/settings-keys.ts @@ -53,6 +53,7 @@ export const SETTINGS_KEYS = { DRY_PENALTY_LAST_N: 'dry_penalty_last_n', // MCP MCP_SERVERS: 'mcpServers', + MCP_REQUEST_TIMEOUT_SECONDS: 'mcpRequestTimeoutSeconds', AGENTIC_MAX_TURNS: 'agenticMaxTurns', ALWAYS_SHOW_AGENTIC_TURNS: 'alwaysShowAgenticTurns', AGENTIC_MAX_TOOL_PREVIEW_LINES: 'agenticMaxToolPreviewLines', diff --git a/tools/ui/src/lib/constants/settings-registry.ts b/tools/ui/src/lib/constants/settings-registry.ts index c4fc3fb30..bdbb17d96 100644 --- a/tools/ui/src/lib/constants/settings-registry.ts +++ b/tools/ui/src/lib/constants/settings-registry.ts @@ -23,7 +23,8 @@ import type { SettingsSectionEntry, SettingsSection } from '$lib/types'; -import { CLI_FLAGS } from '$lib/constants'; +import { CLI_FLAGS, DEFAULT_MCP_CONFIG } from '$lib/constants'; +import McpLogo from '$lib/components/app/mcp/McpLogo.svelte'; import { SETTINGS_KEYS } from './settings-keys'; import { ROUTES, SETTINGS_SECTION_SLUGS } from './routes'; import { TITLE_GENERATION } from './title-generation'; @@ -35,6 +36,7 @@ export const SETTINGS_SECTION_TITLES = { PENALTIES: 'Penalties', AGENTIC: 'Agentic', TOOLS: 'Tools', + MCP: 'MCP', IMPORT_EXPORT: 'Import/Export', DEVELOPER: 'Developer' } as const; @@ -657,6 +659,22 @@ const SETTINGS_REGISTRY: Record = { section: SETTINGS_SECTION_SLUGS.DEVELOPER } ] + }, + [SETTINGS_SECTION_SLUGS.MCP]: { + title: SETTINGS_SECTION_TITLES.MCP, + slug: SETTINGS_SECTION_SLUGS.MCP, + icon: McpLogo, + settings: [ + { + key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS, + label: 'Request timeout (seconds)', + help: 'Default timeout for individual MCP tool calls. Can be overridden per server.', + defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds, + type: SettingsFieldType.INPUT, + section: SETTINGS_SECTION_SLUGS.MCP, + isPositiveInteger: true + } + ] } } as const; @@ -727,6 +745,7 @@ export const SETTINGS_CHAT_SECTIONS: SettingsSection[] = [ label: s.label, type: s.type, isExperimental: s.isExperimental, + isPositiveInteger: s.isPositiveInteger, help: s.help, options: s.options })) diff --git a/tools/ui/src/lib/services/mcp.service.ts b/tools/ui/src/lib/services/mcp.service.ts index 458013b5a..44cbd4a8a 100644 --- a/tools/ui/src/lib/services/mcp.service.ts +++ b/tools/ui/src/lib/services/mcp.service.ts @@ -665,7 +665,9 @@ export class MCPService { tools: [], serverName, transportType, - connectionTimeMs: 0 + connectionTimeMs: 0, + requestTimeoutMs: + serverConfig.requestTimeoutMs ?? DEFAULT_MCP_CONFIG.requestTimeoutSeconds * 1000 }); const connectionTimeMs = Math.round(performance.now() - startTime); @@ -694,7 +696,9 @@ export class MCPService { clientCapabilities: effectiveCapabilities, protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion, instructions, - connectionTimeMs + connectionTimeMs, + requestTimeoutMs: + serverConfig.requestTimeoutMs ?? DEFAULT_MCP_CONFIG.requestTimeoutSeconds * 1000 }; } @@ -813,7 +817,7 @@ export class MCPService { const result = await connection.client.callTool( { name: params.name, arguments: params.arguments }, undefined, - { signal } + { signal, timeout: connection.requestTimeoutMs } ); return { diff --git a/tools/ui/src/lib/stores/mcp.svelte.ts b/tools/ui/src/lib/stores/mcp.svelte.ts index 2a1eb3ff5..8fb306da8 100644 --- a/tools/ui/src/lib/stores/mcp.svelte.ts +++ b/tools/ui/src/lib/stores/mcp.svelte.ts @@ -168,7 +168,9 @@ class MCPStore { enabled: Boolean((entry as { enabled?: unknown })?.enabled), url, name: (entry as { name?: string })?.name, - requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds, + requestTimeoutSeconds: + (entry as { requestTimeoutSeconds?: number })?.requestTimeoutSeconds ?? + DEFAULT_MCP_CONFIG.requestTimeoutSeconds, headers: headers || undefined, useProxy: Boolean((entry as { useProxy?: unknown })?.useProxy) } satisfies MCPServerSettingsEntry; @@ -554,7 +556,8 @@ class MCPStore { url: serverData.url.trim(), name: serverData.name, headers: serverData.headers?.trim() || undefined, - requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds, + requestTimeoutSeconds: + Number(config().mcpRequestTimeoutSeconds) || DEFAULT_MCP_CONFIG.requestTimeoutSeconds, useProxy: serverData.useProxy }; settingsStore.updateConfig(SETTINGS_KEYS.MCP_SERVERS, JSON.stringify([...servers, newServer])); diff --git a/tools/ui/src/lib/types/mcp.d.ts b/tools/ui/src/lib/types/mcp.d.ts index 3837bcdf1..7aa050cdf 100644 --- a/tools/ui/src/lib/types/mcp.d.ts +++ b/tools/ui/src/lib/types/mcp.d.ts @@ -135,6 +135,8 @@ export interface MCPConnection { protocolVersion?: string; instructions?: string; connectionTimeMs: number; + /** Configured timeout for individual requests (tool calls, etc.) in milliseconds */ + requestTimeoutMs: number; } /** diff --git a/tools/ui/src/lib/types/settings.d.ts b/tools/ui/src/lib/types/settings.d.ts index 1ab7a7e5d..65096db34 100644 --- a/tools/ui/src/lib/types/settings.d.ts +++ b/tools/ui/src/lib/types/settings.d.ts @@ -42,6 +42,7 @@ export interface SettingsFieldConfig { label: string; type: SettingsFieldType; isExperimental?: boolean; + isPositiveInteger?: boolean; help?: string; options?: Array<{ value: string; label: string; icon?: typeof Icon }>; } diff --git a/tools/ui/src/lib/utils/mcp.ts b/tools/ui/src/lib/utils/mcp.ts index ee2779845..05fe90048 100644 --- a/tools/ui/src/lib/utils/mcp.ts +++ b/tools/ui/src/lib/utils/mcp.ts @@ -49,7 +49,7 @@ export function detectMcpTransportFromUrl(url: string): MCPTransportType { /** * Parses MCP server settings from a JSON string or array. - * requestTimeoutSeconds is not user-configurable in the UI, so we always use the default value. + * Preserves per-server requestTimeoutSeconds if stored, otherwise falls back to the global default. * @param rawServers - The raw servers to parse * @returns An empty array if the input is invalid. */ @@ -88,7 +88,9 @@ export function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEn enabled: Boolean((entry as { enabled?: unknown })?.enabled), url, name: (entry as { name?: string })?.name, - requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds, + requestTimeoutSeconds: + (entry as { requestTimeoutSeconds?: number })?.requestTimeoutSeconds ?? + DEFAULT_MCP_CONFIG.requestTimeoutSeconds, headers: headers || undefined, useProxy: Boolean((entry as { useProxy?: unknown })?.useProxy) } satisfies MCPServerSettingsEntry;