ui: Add request timeout for MCP tool calls (#23138)
Some checks failed
Check Pre-Tokenizer Hashes / pre-tokenizer-hashes (push) Has been cancelled
Python check requirements.txt / check-requirements (push) Has been cancelled
Python Type-Check / python type-check (push) Has been cancelled

* feat: Add request timeout for MCP tool calls in llama-ui

* feat: MCP Settings tab with max timeout setting
This commit is contained in:
Aleksander Grygier 2026-05-16 15:20:27 +02:00 committed by GitHub
parent 3a92bc99db
commit 0253fb21f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 43 additions and 8 deletions

View file

@ -79,6 +79,8 @@
<div class="relative w-full">
<Input
id={field.key}
type={field.isPositiveInteger ? 'number' : 'text'}
{...field.isPositiveInteger ? { min: '1', step: '1' } : {}}
value={currentValue}
oninput={(e) => {
// Update local config immediately for real-time badge feedback

View file

@ -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;

View file

@ -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',

View file

@ -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<string, SettingsSectionEntry> = {
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
}))

View file

@ -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 {

View file

@ -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]));

View file

@ -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;
}
/**

View file

@ -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 }>;
}

View file

@ -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;