diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz
index f1ccf5a755..88670253fb 100644
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte
index b9bb5b7e3f..db0bd18fa0 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAttachmentsDropdown.svelte
@@ -1,7 +1,19 @@
@@ -234,6 +291,112 @@
+
+
+
+
+ Tools
+
+
+
+ {#if totalToolCount === 0}
+
+ {#if toolsStore.loading}
+
+ Loading tools...
+ {:else if toolsStore.error}
+ Failed to load tools
+ {:else}
+ No tools available
+ {/if}
+
+ {:else}
+
+ {enabledToolCount}/{totalToolCount} tools enabled
+
+
+
+ {#each groups as group (group.label)}
+ {@const isExpanded = expandedGroups.has(group.label)}
+ {@const { checked, indeterminate } = getGroupCheckedState(group)}
+ {@const favicon = getFavicon(group)}
+
+
{
+ if (expandedGroups.has(group.label)) {
+ expandedGroups.delete(group.label);
+ } else {
+ expandedGroups.add(group.label);
+ }
+ }}
+ >
+
+
+ {#if isExpanded}
+
+ {:else}
+
+ {/if}
+
+
+ {#if favicon}
+
{
+ (e.currentTarget as HTMLImageElement).style.display = 'none';
+ }}
+ />
+ {/if}
+
+ {group.label}
+
+
+
+ {group.tools.length}
+
+
+
+
toolsStore.toggleGroup(group)}
+ class="mr-2 h-4 w-4 shrink-0"
+ />
+
+
+
+
+ {#each group.tools as tool (tool.function.name)}
+
+ {/each}
+
+
+
+ {/each}
+
+ {/if}
+
+
+
@@ -272,7 +435,7 @@
/>
{/if}
- {getServerLabel(server)}
+ {mcpStore.getServerLabel(server)}
{#if hasError}
{
return apiFetch(API_TOOLS.LIST);
}
+
+ /**
+ * Execute a built-in tool on the server.
+ */
+ static async executeTool(
+ toolName: string,
+ params: Record,
+ signal?: AbortSignal
+ ): Promise {
+ const result = await apiFetch>(API_TOOLS.EXECUTE, {
+ method: 'POST',
+ body: JSON.stringify({ tool: toolName, params }),
+ signal
+ });
+ const isError = 'error' in result;
+ const content = isError ? String(result.error) : JSON.stringify(result);
+ return { content, isError };
+ }
}
diff --git a/tools/server/webui/src/lib/stores/agentic.svelte.ts b/tools/server/webui/src/lib/stores/agentic.svelte.ts
index f8834f9df3..41d8527678 100644
--- a/tools/server/webui/src/lib/stores/agentic.svelte.ts
+++ b/tools/server/webui/src/lib/stores/agentic.svelte.ts
@@ -21,6 +21,8 @@ import { ChatService } from '$lib/services';
import { config } from '$lib/stores/settings.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { modelsStore } from '$lib/stores/models.svelte';
+import { toolsStore, ToolSource } from '$lib/stores/tools.svelte';
+import { ToolsService } from '$lib/services/tools.service';
import { isAbortError } from '$lib/utils';
import {
DEFAULT_AGENTIC_CONFIG,
@@ -184,13 +186,24 @@ class AgenticStore {
const maxTurns = Number(settings.agenticMaxTurns) || DEFAULT_AGENTIC_CONFIG.maxTurns;
const maxToolPreviewLines =
Number(settings.agenticMaxToolPreviewLines) || DEFAULT_AGENTIC_CONFIG.maxToolPreviewLines;
+ const hasTools =
+ mcpStore.hasEnabledServers(perChatOverrides) ||
+ toolsStore.builtinTools.length > 0 ||
+ toolsStore.customTools.length > 0;
return {
- enabled: mcpStore.hasEnabledServers(perChatOverrides) && DEFAULT_AGENTIC_CONFIG.enabled,
+ enabled: hasTools && DEFAULT_AGENTIC_CONFIG.enabled,
maxTurns,
maxToolPreviewLines
};
}
+ private parseToolArguments(args: string | Record): Record {
+ if (typeof args === 'object') return args;
+ const trimmed = args.trim();
+ if (trimmed === '') return {};
+ return JSON.parse(trimmed) as Record;
+ }
+
async runAgenticFlow(params: AgenticFlowParams): Promise {
const { conversationId, messages, options = {}, callbacks, signal, perChatOverrides } = params;
const {
@@ -208,15 +221,24 @@ class AgenticStore {
const agenticConfig = this.getConfig(config(), perChatOverrides);
if (!agenticConfig.enabled) return { handled: false };
- const initialized = await mcpStore.ensureInitialized(perChatOverrides);
- if (!initialized) {
- console.log('[AgenticStore] MCP not initialized, falling back to standard chat');
- return { handled: false };
+ const hasMcpServers = mcpStore.hasEnabledServers(perChatOverrides);
+ if (hasMcpServers) {
+ const initialized = await mcpStore.ensureInitialized(perChatOverrides);
+
+ if (!initialized) {
+ console.log('[AgenticStore] MCP not initialized');
+ }
}
- const tools = mcpStore.getToolDefinitionsForLLM();
+ // Ensure built-in tools are fetched
+ if (toolsStore.builtinTools.length === 0 && !toolsStore.loading) {
+ await toolsStore.fetchBuiltinTools();
+ }
+
+ const tools = toolsStore.getEnabledToolsForLLM();
if (tools.length === 0) {
console.log('[AgenticStore] No tools available, falling back to standard chat');
+
return { handled: false };
}
@@ -244,7 +266,8 @@ class AgenticStore {
totalToolCalls: 0,
lastError: null
});
- mcpStore.acquireConnection();
+
+ if (hasMcpServers) mcpStore.acquireConnection();
try {
await this.executeAgenticLoop({
@@ -274,11 +297,14 @@ class AgenticStore {
return { handled: true, error: normalizedError };
} finally {
this.updateSession(conversationId, { isRunning: false });
- await mcpStore
- .releaseConnection()
- .catch((err: unknown) =>
- console.warn('[AgenticStore] Failed to release MCP connection:', err)
- );
+
+ if (hasMcpServers) {
+ await mcpStore
+ .releaseConnection()
+ .catch((err: unknown) =>
+ console.warn('[AgenticStore] Failed to release MCP connection:', err)
+ );
+ }
}
}
@@ -517,17 +543,29 @@ class AgenticStore {
}
const toolStartTime = performance.now();
- const mcpCall: MCPToolCall = {
- id: toolCall.id,
- function: { name: toolCall.function.name, arguments: toolCall.function.arguments }
- };
+ const toolName = toolCall.function.name;
+ const toolSource = toolsStore.getToolSource(toolName);
let result: string;
let toolSuccess = true;
try {
- const executionResult = await mcpStore.executeTool(mcpCall, signal);
- result = executionResult.content;
+ if (toolSource === ToolSource.BUILTIN) {
+ const args = this.parseToolArguments(toolCall.function.arguments);
+ const executionResult = await ToolsService.executeTool(toolName, args, signal);
+
+ result = executionResult.content;
+
+ if (executionResult.isError) toolSuccess = false;
+ } else {
+ const mcpCall: MCPToolCall = {
+ id: toolCall.id,
+ function: { name: toolName, arguments: toolCall.function.arguments }
+ };
+ const executionResult = await mcpStore.executeTool(mcpCall, signal);
+
+ result = executionResult.content;
+ }
} catch (error) {
if (isAbortError(error)) {
onComplete?.(
diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts
index c31dfc8cbf..54563112fb 100644
--- a/tools/server/webui/src/lib/stores/chat.svelte.ts
+++ b/tools/server/webui/src/lib/stores/chat.svelte.ts
@@ -17,6 +17,7 @@ import { conversationsStore } from '$lib/stores/conversations.svelte';
import { config } from '$lib/stores/settings.svelte';
import { agenticStore } from '$lib/stores/agentic.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
+import { toolsStore } from '$lib/stores/tools.svelte';
import { contextSize, isRouterMode } from '$lib/stores/server.svelte';
import {
selectedModelName,
@@ -731,10 +732,12 @@ class ChatStore {
if (agenticResult.handled) return;
}
+ const enabledTools = toolsStore.getEnabledToolsForLLM();
const completionOptions = {
...this.getApiOptions(),
...(effectiveModel ? { model: effectiveModel } : {}),
- ...streamCallbacks
+ ...streamCallbacks,
+ ...(enabledTools.length > 0 ? { tools: enabledTools } : {})
};
await ChatService.sendMessage(
diff --git a/tools/server/webui/src/lib/stores/tools.svelte.ts b/tools/server/webui/src/lib/stores/tools.svelte.ts
new file mode 100644
index 0000000000..5d19d605aa
--- /dev/null
+++ b/tools/server/webui/src/lib/stores/tools.svelte.ts
@@ -0,0 +1,369 @@
+import type { OpenAIToolDefinition } from '$lib/types';
+import { ToolsService } from '$lib/services/tools.service';
+import { mcpStore } from '$lib/stores/mcp.svelte';
+import { HealthCheckStatus } from '$lib/enums';
+import { config } from '$lib/stores/settings.svelte';
+import { DISABLED_TOOLS_LOCALSTORAGE_KEY } from '$lib/constants';
+import { SvelteSet } from 'svelte/reactivity';
+
+export enum ToolSource {
+ BUILTIN = 'builtin',
+ MCP = 'mcp',
+ CUSTOM = 'custom'
+}
+
+export interface ToolEntry {
+ source: ToolSource;
+ /** For MCP tools, the server ID; otherwise undefined */
+ serverName?: string;
+ definition: OpenAIToolDefinition;
+}
+
+export interface ToolGroup {
+ source: ToolSource;
+ label: string;
+ /** For MCP groups, the server ID */
+ serverId?: string;
+ tools: OpenAIToolDefinition[];
+}
+
+class ToolsStore {
+ private _builtinTools = $state([]);
+ private _loading = $state(false);
+ private _error = $state(null);
+ private _disabledTools = $state(new SvelteSet());
+
+ constructor() {
+ try {
+ const stored = localStorage.getItem(DISABLED_TOOLS_LOCALSTORAGE_KEY);
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ if (Array.isArray(parsed)) {
+ for (const name of parsed) {
+ if (typeof name === 'string') this._disabledTools.add(name);
+ }
+ }
+ }
+ } catch {
+ throw new Error('Failed to load disabled tools from localStorage');
+ }
+ }
+
+ private persistDisabledTools(): void {
+ try {
+ localStorage.setItem(
+ DISABLED_TOOLS_LOCALSTORAGE_KEY,
+ JSON.stringify([...this._disabledTools])
+ );
+ } catch {
+ // ignore storage errors
+ }
+ }
+
+ get builtinTools(): OpenAIToolDefinition[] {
+ return this._builtinTools;
+ }
+
+ get mcpTools(): OpenAIToolDefinition[] {
+ return mcpStore.getToolDefinitionsForLLM();
+ }
+
+ get customTools(): OpenAIToolDefinition[] {
+ const raw = config().custom;
+ if (!raw || typeof raw !== 'string') return [];
+
+ try {
+ const parsed = JSON.parse(raw);
+ if (!Array.isArray(parsed)) return [];
+
+ return parsed.filter(
+ (t: unknown): t is OpenAIToolDefinition =>
+ typeof t === 'object' &&
+ t !== null &&
+ 'type' in t &&
+ (t as OpenAIToolDefinition).type === 'function' &&
+ 'function' in t &&
+ typeof (t as OpenAIToolDefinition).function?.name === 'string'
+ );
+ } catch {
+ return [];
+ }
+ }
+
+ /** Flat list of all tool entries with source metadata */
+ get allTools(): ToolEntry[] {
+ const entries: ToolEntry[] = [];
+
+ for (const def of this._builtinTools) {
+ entries.push({ source: ToolSource.BUILTIN, definition: def });
+ }
+
+ // Use live connections when available (full schema), fall back to health check data
+ const connections = mcpStore.getConnections();
+ if (connections.size > 0) {
+ for (const [serverId, connection] of connections) {
+ const serverName = mcpStore.getServerDisplayName(serverId);
+ for (const tool of connection.tools) {
+ const rawSchema = (tool.inputSchema as Record) ?? {
+ type: 'object',
+ properties: {},
+ required: []
+ };
+ entries.push({
+ source: ToolSource.MCP,
+ serverName,
+ definition: {
+ type: 'function',
+ function: {
+ name: tool.name,
+ description: tool.description,
+ parameters: rawSchema
+ }
+ }
+ });
+ }
+ }
+ } else {
+ for (const { serverName, tools } of this.getMcpToolsFromHealthChecks()) {
+ for (const tool of tools) {
+ entries.push({
+ source: ToolSource.MCP,
+ serverName,
+ definition: {
+ type: 'function',
+ function: {
+ name: tool.name,
+ description: tool.description,
+ parameters: { type: 'object', properties: {}, required: [] }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ for (const def of this.customTools) {
+ entries.push({ source: ToolSource.CUSTOM, definition: def });
+ }
+
+ return entries;
+ }
+
+ /** Tools grouped by category for tree display */
+ get toolGroups(): ToolGroup[] {
+ const groups: ToolGroup[] = [];
+
+ if (this._builtinTools.length > 0) {
+ groups.push({
+ source: ToolSource.BUILTIN,
+ label: 'Built-in',
+ tools: this._builtinTools
+ });
+ }
+
+ // Use live connections when available, fall back to health check data
+ const connections = mcpStore.getConnections();
+ if (connections.size > 0) {
+ for (const [serverId, connection] of connections) {
+ if (connection.tools.length === 0) continue;
+ const label = mcpStore.getServerDisplayName(serverId);
+ const tools: OpenAIToolDefinition[] = connection.tools.map((tool) => {
+ const rawSchema = (tool.inputSchema as Record) ?? {
+ type: 'object',
+ properties: {},
+ required: []
+ };
+ return {
+ type: 'function' as const,
+ function: {
+ name: tool.name,
+ description: tool.description,
+ parameters: rawSchema
+ }
+ };
+ });
+ groups.push({ source: ToolSource.MCP, label, serverId, tools });
+ }
+ } else {
+ for (const { serverId, serverName, tools } of this.getMcpToolsFromHealthChecks()) {
+ if (tools.length === 0) continue;
+ const defs: OpenAIToolDefinition[] = tools.map((tool) => ({
+ type: 'function' as const,
+ function: {
+ name: tool.name,
+ description: tool.description,
+ parameters: { type: 'object', properties: {}, required: [] }
+ }
+ }));
+ groups.push({ source: ToolSource.MCP, label: serverName, serverId, tools: defs });
+ }
+ }
+
+ const custom = this.customTools;
+ if (custom.length > 0) {
+ groups.push({
+ source: ToolSource.CUSTOM,
+ label: 'JSON Schema',
+ tools: custom
+ });
+ }
+
+ return groups;
+ }
+
+ /** Only enabled tool definitions (for sending to the API) */
+ get enabledToolDefinitions(): OpenAIToolDefinition[] {
+ return this.allTools
+ .filter((t) => !this._disabledTools.has(t.definition.function.name))
+ .map((t) => t.definition);
+ }
+
+ /**
+ * Returns enabled tool definitions for sending to the LLM.
+ * MCP tools use properly normalized schemas from mcpStore.
+ * Filters out tools disabled via the UI checkboxes.
+ */
+ getEnabledToolsForLLM(): OpenAIToolDefinition[] {
+ const disabled = this._disabledTools;
+ const result: OpenAIToolDefinition[] = [];
+
+ for (const tool of this._builtinTools) {
+ if (!disabled.has(tool.function.name)) {
+ result.push(tool);
+ }
+ }
+
+ // MCP tools with properly normalized schemas
+ for (const tool of mcpStore.getToolDefinitionsForLLM()) {
+ if (!disabled.has(tool.function.name)) {
+ result.push(tool);
+ }
+ }
+
+ for (const tool of this.customTools) {
+ if (!disabled.has(tool.function.name)) {
+ result.push(tool);
+ }
+ }
+
+ return result;
+ }
+
+ get allToolDefinitions(): OpenAIToolDefinition[] {
+ return this.allTools.map((t) => t.definition);
+ }
+
+ get loading(): boolean {
+ return this._loading;
+ }
+
+ get error(): string | null {
+ return this._error;
+ }
+
+ get disabledTools(): SvelteSet {
+ return this._disabledTools;
+ }
+
+ isToolEnabled(toolName: string): boolean {
+ return !this._disabledTools.has(toolName);
+ }
+
+ toggleTool(toolName: string): void {
+ if (this._disabledTools.has(toolName)) {
+ this._disabledTools.delete(toolName);
+ } else {
+ this._disabledTools.add(toolName);
+ }
+ this.persistDisabledTools();
+ }
+
+ setToolEnabled(toolName: string, enabled: boolean): void {
+ if (enabled) {
+ this._disabledTools.delete(toolName);
+ } else {
+ this._disabledTools.add(toolName);
+ }
+ }
+
+ toggleGroup(group: ToolGroup): void {
+ const allEnabled = group.tools.every((t) => this.isToolEnabled(t.function.name));
+ for (const tool of group.tools) {
+ this.setToolEnabled(tool.function.name, !allEnabled);
+ }
+ this.persistDisabledTools();
+ }
+
+ isGroupFullyEnabled(group: ToolGroup): boolean {
+ return group.tools.length > 0 && group.tools.every((t) => this.isToolEnabled(t.function.name));
+ }
+
+ isGroupPartiallyEnabled(group: ToolGroup): boolean {
+ const enabledCount = group.tools.filter((t) => this.isToolEnabled(t.function.name)).length;
+ return enabledCount > 0 && enabledCount < group.tools.length;
+ }
+
+ /**
+ * Get MCP tools from health check data (reactive).
+ * Used when live connections aren't established yet.
+ */
+ private getMcpToolsFromHealthChecks(): {
+ serverId: string;
+ serverName: string;
+ tools: { name: string; description?: string }[];
+ }[] {
+ const result: ReturnType = [];
+ for (const server of mcpStore.getServersSorted().filter((s) => s.enabled)) {
+ const health = mcpStore.getHealthCheckState(server.id);
+ if (health.status === HealthCheckStatus.SUCCESS && health.tools.length > 0) {
+ result.push({
+ serverId: server.id,
+ serverName: mcpStore.getServerLabel(server),
+ tools: health.tools
+ });
+ }
+ }
+ return result;
+ }
+
+ /** Determine the source of a tool by its name. */
+ getToolSource(toolName: string): ToolSource | null {
+ if (this._builtinTools.some((t) => t.function.name === toolName)) {
+ return ToolSource.BUILTIN;
+ }
+ for (const entry of this.allTools) {
+ if (entry.definition.function.name === toolName) {
+ return entry.source;
+ }
+ }
+ return null;
+ }
+
+ /** Check if there are any enabled tools available (builtin, MCP, or custom). */
+ get hasEnabledTools(): boolean {
+ return this.getEnabledToolsForLLM().length > 0;
+ }
+
+ async fetchBuiltinTools(): Promise {
+ if (this._loading) return;
+
+ this._loading = true;
+ this._error = null;
+
+ try {
+ this._builtinTools = await ToolsService.list();
+ } catch (err) {
+ this._error = err instanceof Error ? err.message : String(err);
+ console.error('[ToolsStore] Failed to fetch built-in tools:', err);
+ } finally {
+ this._loading = false;
+ }
+ }
+}
+
+export const toolsStore = new ToolsStore();
+
+export const allTools = () => toolsStore.allTools;
+export const allToolDefinitions = () => toolsStore.allToolDefinitions;
+export const enabledToolDefinitions = () => toolsStore.enabledToolDefinitions;
+export const toolGroups = () => toolsStore.toolGroups;