Date: Sat, 16 May 2026 15:20:27 +0200
Subject: [PATCH 10/10] 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 3ecf00adce8..069855eebef 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 14416478f08..3b3fceea448 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 b673bff278d..92a57f88acf 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 c4fc3fb301e..bdbb17d962c 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 458013b5acb..44cbd4a8aaf 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 2a1eb3ff51d..8fb306da881 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 3837bcdf1b8..7aa050cdfa7 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 1ab7a7e5d54..65096db3449 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 ee27798455d..05fe90048f0 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;