feat: Add per-chat MCP server overrides
This commit is contained in:
parent
54374edecd
commit
dfce09b34b
|
|
@ -1,14 +1,20 @@
|
||||||
import { hasEnabledMcpServers } from './mcp';
|
import { hasEnabledMcpServers } from './mcp';
|
||||||
import type { SettingsConfigType } from '$lib/types/settings';
|
import type { SettingsConfigType } from '$lib/types/settings';
|
||||||
import type { AgenticConfig } from '$lib/types/agentic';
|
import type { AgenticConfig } from '$lib/types/agentic';
|
||||||
|
import type { McpServerOverride } from '$lib/types/database';
|
||||||
import { DEFAULT_AGENTIC_CONFIG } from '$lib/constants/agentic';
|
import { DEFAULT_AGENTIC_CONFIG } from '$lib/constants/agentic';
|
||||||
import { normalizePositiveNumber } from '$lib/utils/number';
|
import { normalizePositiveNumber } from '$lib/utils/number';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current agentic configuration.
|
* Gets the current agentic configuration.
|
||||||
* Automatically disables agentic mode if no MCP servers are configured.
|
* Automatically disables agentic mode if no MCP servers are configured.
|
||||||
|
* @param settings - Global settings configuration
|
||||||
|
* @param perChatOverrides - Optional per-chat MCP server overrides
|
||||||
*/
|
*/
|
||||||
export function getAgenticConfig(settings: SettingsConfigType): AgenticConfig {
|
export function getAgenticConfig(
|
||||||
|
settings: SettingsConfigType,
|
||||||
|
perChatOverrides?: McpServerOverride[]
|
||||||
|
): AgenticConfig {
|
||||||
const maxTurns = normalizePositiveNumber(
|
const maxTurns = normalizePositiveNumber(
|
||||||
settings.agenticMaxTurns,
|
settings.agenticMaxTurns,
|
||||||
DEFAULT_AGENTIC_CONFIG.maxTurns
|
DEFAULT_AGENTIC_CONFIG.maxTurns
|
||||||
|
|
@ -23,7 +29,7 @@ export function getAgenticConfig(settings: SettingsConfigType): AgenticConfig {
|
||||||
: DEFAULT_AGENTIC_CONFIG.filterReasoningAfterFirstTurn;
|
: DEFAULT_AGENTIC_CONFIG.filterReasoningAfterFirstTurn;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: hasEnabledMcpServers(settings) && DEFAULT_AGENTIC_CONFIG.enabled,
|
enabled: hasEnabledMcpServers(settings, perChatOverrides) && DEFAULT_AGENTIC_CONFIG.enabled,
|
||||||
maxTurns,
|
maxTurns,
|
||||||
maxToolPreviewLines,
|
maxToolPreviewLines,
|
||||||
filterReasoningAfterFirstTurn
|
filterReasoningAfterFirstTurn
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { MCPClientConfig, MCPServerConfig, MCPServerSettingsEntry } from '$lib/types/mcp';
|
import type { MCPClientConfig, MCPServerConfig, MCPServerSettingsEntry } from '$lib/types/mcp';
|
||||||
import type { SettingsConfigType } from '$lib/types/settings';
|
import type { SettingsConfigType } from '$lib/types/settings';
|
||||||
|
import type { McpServerOverride } from '$lib/types/database';
|
||||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||||
import { detectMcpTransportFromUrl, generateMcpServerId } from '$lib/utils/mcp';
|
import { detectMcpTransportFromUrl, generateMcpServerId } from '$lib/utils/mcp';
|
||||||
import { normalizePositiveNumber } from '$lib/utils/number';
|
import { normalizePositiveNumber } from '$lib/utils/number';
|
||||||
|
|
@ -76,11 +77,33 @@ function buildServerConfig(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a server is enabled considering per-chat overrides.
|
||||||
|
* Per-chat override takes precedence over global setting.
|
||||||
|
*/
|
||||||
|
function isServerEnabled(
|
||||||
|
server: MCPServerSettingsEntry,
|
||||||
|
perChatOverrides?: McpServerOverride[]
|
||||||
|
): boolean {
|
||||||
|
if (perChatOverrides) {
|
||||||
|
const override = perChatOverrides.find((o) => o.serverId === server.id);
|
||||||
|
if (override !== undefined) {
|
||||||
|
return override.enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return server.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds MCP client configuration from settings.
|
* Builds MCP client configuration from settings.
|
||||||
* Returns undefined if no valid servers are configured.
|
* Returns undefined if no valid servers are configured.
|
||||||
|
* @param config - Global settings configuration
|
||||||
|
* @param perChatOverrides - Optional per-chat server overrides
|
||||||
*/
|
*/
|
||||||
export function buildMcpClientConfig(config: SettingsConfigType): MCPClientConfig | undefined {
|
export function buildMcpClientConfig(
|
||||||
|
config: SettingsConfigType,
|
||||||
|
perChatOverrides?: McpServerOverride[]
|
||||||
|
): MCPClientConfig | undefined {
|
||||||
const rawServers = parseMcpServerSettings(config.mcpServers);
|
const rawServers = parseMcpServerSettings(config.mcpServers);
|
||||||
|
|
||||||
if (!rawServers.length) {
|
if (!rawServers.length) {
|
||||||
|
|
@ -89,7 +112,7 @@ export function buildMcpClientConfig(config: SettingsConfigType): MCPClientConfi
|
||||||
|
|
||||||
const servers: Record<string, MCPServerConfig> = {};
|
const servers: Record<string, MCPServerConfig> = {};
|
||||||
for (const [index, entry] of rawServers.entries()) {
|
for (const [index, entry] of rawServers.entries()) {
|
||||||
if (!entry.enabled) continue;
|
if (!isServerEnabled(entry, perChatOverrides)) continue;
|
||||||
|
|
||||||
const normalized = buildServerConfig(entry);
|
const normalized = buildServerConfig(entry);
|
||||||
if (normalized) {
|
if (normalized) {
|
||||||
|
|
@ -110,6 +133,55 @@ export function buildMcpClientConfig(config: SettingsConfigType): MCPClientConfi
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasEnabledMcpServers(config: SettingsConfigType): boolean {
|
export function hasEnabledMcpServers(
|
||||||
return Boolean(buildMcpClientConfig(config));
|
config: SettingsConfigType,
|
||||||
|
perChatOverrides?: McpServerOverride[]
|
||||||
|
): boolean {
|
||||||
|
return Boolean(buildMcpClientConfig(config, perChatOverrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// MCP Server Usage Stats
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type McpServerUsageStats = Record<string, number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse MCP server usage stats from settings.
|
||||||
|
*/
|
||||||
|
export function parseMcpServerUsageStats(rawStats: unknown): McpServerUsageStats {
|
||||||
|
if (!rawStats) return {};
|
||||||
|
|
||||||
|
if (typeof rawStats === 'string') {
|
||||||
|
const trimmed = rawStats.trim();
|
||||||
|
if (!trimmed) return {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||||
|
return parsed as McpServerUsageStats;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.warn('[MCP] Failed to parse mcpServerUsageStats JSON, ignoring value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get usage count for a specific server.
|
||||||
|
*/
|
||||||
|
export function getMcpServerUsageCount(config: SettingsConfigType, serverId: string): number {
|
||||||
|
const stats = parseMcpServerUsageStats(config.mcpServerUsageStats);
|
||||||
|
return stats[serverId] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment usage count for a server and return updated stats JSON.
|
||||||
|
*/
|
||||||
|
export function incrementMcpServerUsage(config: SettingsConfigType, serverId: string): string {
|
||||||
|
const stats = parseMcpServerUsageStats(config.mcpServerUsageStats);
|
||||||
|
stats[serverId] = (stats[serverId] || 0) + 1;
|
||||||
|
return JSON.stringify(stats);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> =
|
||||||
autoShowSidebarOnNewChat: true,
|
autoShowSidebarOnNewChat: true,
|
||||||
autoMicOnEmpty: false,
|
autoMicOnEmpty: false,
|
||||||
mcpServers: '[]',
|
mcpServers: '[]',
|
||||||
|
mcpServerUsageStats: '{}', // JSON object: { [serverId]: usageCount }
|
||||||
agenticMaxTurns: 10,
|
agenticMaxTurns: 10,
|
||||||
agenticMaxToolPreviewLines: 25,
|
agenticMaxToolPreviewLines: 25,
|
||||||
agenticFilterReasoningAfterFirstTurn: true,
|
agenticFilterReasoningAfterFirstTurn: true,
|
||||||
|
showToolCallInProgress: false,
|
||||||
// make sure these default values are in sync with `common.h`
|
// make sure these default values are in sync with `common.h`
|
||||||
samplers: 'top_k;typ_p;top_p;min_p;temperature',
|
samplers: 'top_k;typ_p;top_p;min_p;temperature',
|
||||||
backend_sampling: false,
|
backend_sampling: false,
|
||||||
|
|
@ -113,12 +115,16 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
|
||||||
'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
|
'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
|
||||||
mcpServers:
|
mcpServers:
|
||||||
'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
|
'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
|
||||||
|
mcpServerUsageStats:
|
||||||
|
'Usage statistics for MCP servers. Tracks how many times tools from each server have been used.',
|
||||||
agenticMaxTurns:
|
agenticMaxTurns:
|
||||||
'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
|
'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
|
||||||
agenticMaxToolPreviewLines:
|
agenticMaxToolPreviewLines:
|
||||||
'Number of lines shown in tool output previews (last N lines). Only these previews and the final LLM response persist after the agentic loop completes.',
|
'Number of lines shown in tool output previews (last N lines). Only these previews and the final LLM response persist after the agentic loop completes.',
|
||||||
agenticFilterReasoningAfterFirstTurn:
|
agenticFilterReasoningAfterFirstTurn:
|
||||||
'Only show reasoning from the first agentic turn. When disabled, reasoning from all turns is merged in one (WebUI limitation).',
|
'Only show reasoning from the first agentic turn. When disabled, reasoning from all turns is merged in one (WebUI limitation).',
|
||||||
|
showToolCallInProgress:
|
||||||
|
'Automatically expand tool call details while executing and keep them expanded after completion.',
|
||||||
pyInterpreterEnabled:
|
pyInterpreterEnabled:
|
||||||
'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
|
'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
|
||||||
enableContinueGeneration:
|
enableContinueGeneration:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
import type { ApiChatCompletionToolCall, ApiChatMessageData } from '$lib/types/api';
|
import type { ApiChatCompletionToolCall, ApiChatMessageData } from '$lib/types/api';
|
||||||
import type { ChatMessagePromptProgress, ChatMessageTimings } from '$lib/types/chat';
|
import type { ChatMessagePromptProgress, ChatMessageTimings } from '$lib/types/chat';
|
||||||
import type { MCPToolCall } from '$lib/types/mcp';
|
import type { MCPToolCall } from '$lib/types/mcp';
|
||||||
import type { DatabaseMessage, DatabaseMessageExtra } from '$lib/types/database';
|
import type { DatabaseMessage, DatabaseMessageExtra, McpServerOverride } from '$lib/types/database';
|
||||||
import { getAgenticConfig } from '$lib/config/agentic';
|
import { getAgenticConfig } from '$lib/config/agentic';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { getAuthHeaders } from '$lib/utils';
|
import { getAuthHeaders } from '$lib/utils';
|
||||||
|
|
@ -69,6 +69,8 @@ export interface AgenticFlowParams {
|
||||||
options?: AgenticFlowOptions;
|
options?: AgenticFlowOptions;
|
||||||
callbacks: AgenticFlowCallbacks;
|
callbacks: AgenticFlowCallbacks;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
|
/** Per-chat MCP server overrides */
|
||||||
|
perChatOverrides?: McpServerOverride[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgenticFlowResult {
|
export interface AgenticFlowResult {
|
||||||
|
|
@ -126,18 +128,18 @@ class AgenticStore {
|
||||||
* @returns AgenticFlowResult indicating if the flow handled the request
|
* @returns AgenticFlowResult indicating if the flow handled the request
|
||||||
*/
|
*/
|
||||||
async runAgenticFlow(params: AgenticFlowParams): Promise<AgenticFlowResult> {
|
async runAgenticFlow(params: AgenticFlowParams): Promise<AgenticFlowResult> {
|
||||||
const { messages, options = {}, callbacks, signal } = params;
|
const { messages, options = {}, callbacks, signal, perChatOverrides } = params;
|
||||||
const { onChunk, onReasoningChunk, onToolCallChunk, onModel, onComplete, onError, onTimings } =
|
const { onChunk, onReasoningChunk, onToolCallChunk, onModel, onComplete, onError, onTimings } =
|
||||||
callbacks;
|
callbacks;
|
||||||
|
|
||||||
// Get agentic configuration
|
// Get agentic configuration (considering per-chat MCP overrides)
|
||||||
const agenticConfig = getAgenticConfig(config());
|
const agenticConfig = getAgenticConfig(config(), perChatOverrides);
|
||||||
if (!agenticConfig.enabled) {
|
if (!agenticConfig.enabled) {
|
||||||
return { handled: false };
|
return { handled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure MCP is initialized
|
// Ensure MCP is initialized with per-chat overrides
|
||||||
const hostManager = await mcpStore.ensureInitialized();
|
const hostManager = await mcpStore.ensureInitialized(perChatOverrides);
|
||||||
if (!hostManager) {
|
if (!hostManager) {
|
||||||
console.log('[AgenticStore] MCP not initialized, falling back to standard chat');
|
console.log('[AgenticStore] MCP not initialized, falling back to standard chat');
|
||||||
return { handled: false };
|
return { handled: false };
|
||||||
|
|
|
||||||
|
|
@ -808,8 +808,9 @@ class ChatStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try agentic flow first if enabled
|
// Try agentic flow first if enabled (considering per-chat MCP overrides)
|
||||||
const agenticConfig = getAgenticConfig(config());
|
const perChatOverrides = conversationsStore.activeConversation?.mcpServerOverrides;
|
||||||
|
const agenticConfig = getAgenticConfig(config(), perChatOverrides);
|
||||||
if (agenticConfig.enabled) {
|
if (agenticConfig.enabled) {
|
||||||
const agenticResult = await agenticStore.runAgenticFlow({
|
const agenticResult = await agenticStore.runAgenticFlow({
|
||||||
messages: allMessages,
|
messages: allMessages,
|
||||||
|
|
@ -818,7 +819,8 @@ class ChatStore {
|
||||||
...(modelOverride ? { model: modelOverride } : {})
|
...(modelOverride ? { model: modelOverride } : {})
|
||||||
},
|
},
|
||||||
callbacks: streamCallbacks,
|
callbacks: streamCallbacks,
|
||||||
signal: abortController.signal
|
signal: abortController.signal,
|
||||||
|
perChatOverrides
|
||||||
});
|
});
|
||||||
|
|
||||||
if (agenticResult.handled) {
|
if (agenticResult.handled) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { DatabaseService } from '$lib/services/database';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config } from '$lib/stores/settings.svelte';
|
||||||
import { filterByLeafNodeId, findLeafNode } from '$lib/utils';
|
import { filterByLeafNodeId, findLeafNode } from '$lib/utils';
|
||||||
import { AttachmentType } from '$lib/enums';
|
import { AttachmentType } from '$lib/enums';
|
||||||
|
import type { McpServerOverride } from '$lib/types/database';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* conversationsStore - Persistent conversation data and lifecycle management
|
* conversationsStore - Persistent conversation data and lifecycle management
|
||||||
|
|
@ -62,6 +63,9 @@ class ConversationsStore {
|
||||||
/** Whether the store has been initialized */
|
/** Whether the store has been initialized */
|
||||||
isInitialized = $state(false);
|
isInitialized = $state(false);
|
||||||
|
|
||||||
|
/** Pending MCP server overrides for new conversations (before first message) */
|
||||||
|
pendingMcpServerOverrides = $state<McpServerOverride[]>([]);
|
||||||
|
|
||||||
/** Callback for title update confirmation dialog */
|
/** Callback for title update confirmation dialog */
|
||||||
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
|
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
|
||||||
|
|
||||||
|
|
@ -170,6 +174,20 @@ class ConversationsStore {
|
||||||
const conversationName = name || `Chat ${new Date().toLocaleString()}`;
|
const conversationName = name || `Chat ${new Date().toLocaleString()}`;
|
||||||
const conversation = await DatabaseService.createConversation(conversationName);
|
const conversation = await DatabaseService.createConversation(conversationName);
|
||||||
|
|
||||||
|
// Apply any pending MCP server overrides to the new conversation
|
||||||
|
if (this.pendingMcpServerOverrides.length > 0) {
|
||||||
|
// Deep clone to plain objects (Svelte 5 $state uses Proxies which can't be cloned to IndexedDB)
|
||||||
|
const plainOverrides = this.pendingMcpServerOverrides.map((o) => ({
|
||||||
|
serverId: o.serverId,
|
||||||
|
enabled: o.enabled
|
||||||
|
}));
|
||||||
|
conversation.mcpServerOverrides = plainOverrides;
|
||||||
|
await DatabaseService.updateConversation(conversation.id, {
|
||||||
|
mcpServerOverrides: plainOverrides
|
||||||
|
});
|
||||||
|
this.pendingMcpServerOverrides = []; // Clear pending overrides
|
||||||
|
}
|
||||||
|
|
||||||
this.conversations.unshift(conversation);
|
this.conversations.unshift(conversation);
|
||||||
this.activeConversation = conversation;
|
this.activeConversation = conversation;
|
||||||
this.activeMessages = [];
|
this.activeMessages = [];
|
||||||
|
|
@ -192,6 +210,8 @@ class ConversationsStore {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear pending overrides when switching to an existing conversation
|
||||||
|
this.pendingMcpServerOverrides = [];
|
||||||
this.activeConversation = conversation;
|
this.activeConversation = conversation;
|
||||||
|
|
||||||
if (conversation.currNode) {
|
if (conversation.currNode) {
|
||||||
|
|
@ -333,6 +353,150 @@ class ConversationsStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// MCP Server Per-Chat Overrides
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets MCP server override for a specific server in the active conversation.
|
||||||
|
* Falls back to pending overrides if no active conversation exists.
|
||||||
|
* @param serverId - The server ID to check
|
||||||
|
* @returns The override if set, undefined if using global setting
|
||||||
|
*/
|
||||||
|
getMcpServerOverride(serverId: string): McpServerOverride | undefined {
|
||||||
|
if (this.activeConversation) {
|
||||||
|
return this.activeConversation.mcpServerOverrides?.find(
|
||||||
|
(o: McpServerOverride) => o.serverId === serverId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Fall back to pending overrides if no active conversation
|
||||||
|
return this.pendingMcpServerOverrides.find((o) => o.serverId === serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an MCP server is enabled for the active conversation.
|
||||||
|
* Per-chat override takes precedence over global setting.
|
||||||
|
* @param serverId - The server ID to check
|
||||||
|
* @param globalEnabled - The global enabled state from settings
|
||||||
|
* @returns True if server is enabled for this conversation
|
||||||
|
*/
|
||||||
|
isMcpServerEnabledForChat(serverId: string, globalEnabled: boolean): boolean {
|
||||||
|
const override = this.getMcpServerOverride(serverId);
|
||||||
|
return override !== undefined ? override.enabled : globalEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets or removes MCP server override for the active conversation.
|
||||||
|
* If no conversation exists, stores as pending override (applied when conversation is created).
|
||||||
|
* @param serverId - The server ID to override
|
||||||
|
* @param enabled - The enabled state, or undefined to remove override (use global)
|
||||||
|
*/
|
||||||
|
async setMcpServerOverride(serverId: string, enabled: boolean | undefined): Promise<void> {
|
||||||
|
// If no active conversation, store as pending override
|
||||||
|
if (!this.activeConversation) {
|
||||||
|
this.setPendingMcpServerOverride(serverId, enabled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone to plain objects to avoid Proxy serialization issues with IndexedDB
|
||||||
|
const currentOverrides = (this.activeConversation.mcpServerOverrides || []).map(
|
||||||
|
(o: McpServerOverride) => ({
|
||||||
|
serverId: o.serverId,
|
||||||
|
enabled: o.enabled
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let newOverrides: McpServerOverride[];
|
||||||
|
|
||||||
|
if (enabled === undefined) {
|
||||||
|
// Remove override - use global setting
|
||||||
|
newOverrides = currentOverrides.filter((o: McpServerOverride) => o.serverId !== serverId);
|
||||||
|
} else {
|
||||||
|
// Set or update override
|
||||||
|
const existingIndex = currentOverrides.findIndex(
|
||||||
|
(o: McpServerOverride) => o.serverId === serverId
|
||||||
|
);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
newOverrides = [...currentOverrides];
|
||||||
|
newOverrides[existingIndex] = { serverId, enabled };
|
||||||
|
} else {
|
||||||
|
newOverrides = [...currentOverrides, { serverId, enabled }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update in database (plain objects, not proxies)
|
||||||
|
await DatabaseService.updateConversation(this.activeConversation.id, {
|
||||||
|
mcpServerOverrides: newOverrides.length > 0 ? newOverrides : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
this.activeConversation.mcpServerOverrides = newOverrides.length > 0 ? newOverrides : undefined;
|
||||||
|
|
||||||
|
// Also update in conversations list
|
||||||
|
const convIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id);
|
||||||
|
if (convIndex !== -1) {
|
||||||
|
this.conversations[convIndex].mcpServerOverrides =
|
||||||
|
newOverrides.length > 0 ? newOverrides : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles MCP server enabled state for the active conversation.
|
||||||
|
* Creates a per-chat override that differs from the global setting.
|
||||||
|
* @param serverId - The server ID to toggle
|
||||||
|
* @param globalEnabled - The global enabled state from settings
|
||||||
|
*/
|
||||||
|
async toggleMcpServerForChat(serverId: string, globalEnabled: boolean): Promise<void> {
|
||||||
|
const currentEnabled = this.isMcpServerEnabledForChat(serverId, globalEnabled);
|
||||||
|
await this.setMcpServerOverride(serverId, !currentEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets MCP server to use global setting (removes per-chat override).
|
||||||
|
* @param serverId - The server ID to reset
|
||||||
|
*/
|
||||||
|
async resetMcpServerToGlobal(serverId: string): Promise<void> {
|
||||||
|
await this.setMcpServerOverride(serverId, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets or removes a pending MCP server override (for new conversations).
|
||||||
|
* @param serverId - The server ID to override
|
||||||
|
* @param enabled - The enabled state, or undefined to remove override
|
||||||
|
*/
|
||||||
|
private setPendingMcpServerOverride(serverId: string, enabled: boolean | undefined): void {
|
||||||
|
if (enabled === undefined) {
|
||||||
|
// Remove pending override
|
||||||
|
this.pendingMcpServerOverrides = this.pendingMcpServerOverrides.filter(
|
||||||
|
(o) => o.serverId !== serverId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Set or update pending override
|
||||||
|
const existingIndex = this.pendingMcpServerOverrides.findIndex(
|
||||||
|
(o) => o.serverId === serverId
|
||||||
|
);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
this.pendingMcpServerOverrides[existingIndex] = { serverId, enabled };
|
||||||
|
} else {
|
||||||
|
this.pendingMcpServerOverrides = [...this.pendingMcpServerOverrides, { serverId, enabled }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a pending MCP server override.
|
||||||
|
* @param serverId - The server ID to check
|
||||||
|
*/
|
||||||
|
private getPendingMcpServerOverride(serverId: string): McpServerOverride | undefined {
|
||||||
|
return this.pendingMcpServerOverrides.find((o) => o.serverId === serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all pending MCP server overrides.
|
||||||
|
*/
|
||||||
|
clearPendingMcpServerOverrides(): void {
|
||||||
|
this.pendingMcpServerOverrides = [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to a specific sibling branch by updating currNode and refreshing messages
|
* Navigates to a specific sibling branch by updating currNode and refreshing messages
|
||||||
* @param siblingId - The sibling message ID to navigate to
|
* @param siblingId - The sibling message ID to navigate to
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import {
|
||||||
type ServerStatus
|
type ServerStatus
|
||||||
} from '$lib/mcp/host-manager';
|
} from '$lib/mcp/host-manager';
|
||||||
import type { ToolExecutionResult } from '$lib/mcp/server-connection';
|
import type { ToolExecutionResult } from '$lib/mcp/server-connection';
|
||||||
import { buildMcpClientConfig } from '$lib/config/mcp';
|
import { buildMcpClientConfig, incrementMcpServerUsage } from '$lib/config/mcp';
|
||||||
import { config } from '$lib/stores/settings.svelte';
|
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import type { MCPToolCall } from '$lib/types/mcp';
|
import type { MCPToolCall } from '$lib/types/mcp';
|
||||||
|
import type { McpServerOverride } from '$lib/types/database';
|
||||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||||
import { MCPClient } from '$lib/mcp';
|
import { MCPClient } from '$lib/mcp';
|
||||||
import { detectMcpTransportFromUrl } from '$lib/utils/mcp';
|
import { detectMcpTransportFromUrl } from '$lib/utils/mcp';
|
||||||
|
|
@ -139,11 +140,14 @@ class MCPStore {
|
||||||
* Ensure MCP host manager is initialized with current config.
|
* Ensure MCP host manager is initialized with current config.
|
||||||
* Returns the host manager if successful, undefined otherwise.
|
* Returns the host manager if successful, undefined otherwise.
|
||||||
* Handles config changes by reinitializing as needed.
|
* Handles config changes by reinitializing as needed.
|
||||||
|
* @param perChatOverrides - Optional per-chat MCP server overrides
|
||||||
*/
|
*/
|
||||||
async ensureInitialized(): Promise<MCPHostManager | undefined> {
|
async ensureInitialized(
|
||||||
|
perChatOverrides?: McpServerOverride[]
|
||||||
|
): Promise<MCPHostManager | undefined> {
|
||||||
if (!browser) return undefined;
|
if (!browser) return undefined;
|
||||||
|
|
||||||
const mcpConfig = buildMcpClientConfig(config());
|
const mcpConfig = buildMcpClientConfig(config(), perChatOverrides);
|
||||||
const signature = mcpConfig ? JSON.stringify(mcpConfig) : null;
|
const signature = mcpConfig ? JSON.stringify(mcpConfig) : null;
|
||||||
|
|
||||||
// No config - shutdown if needed
|
// No config - shutdown if needed
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
import type { ChatMessageTimings, ChatRole, ChatMessageType } from '$lib/types/chat';
|
import type { ChatMessageTimings, ChatRole, ChatMessageType } from '$lib/types/chat';
|
||||||
import { AttachmentType } from '$lib/enums';
|
import { AttachmentType } from '$lib/enums';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-chat MCP server override - allows enabling/disabling servers for specific conversations.
|
||||||
|
* If undefined for a server, the global setting is used.
|
||||||
|
*/
|
||||||
|
export interface McpServerOverride {
|
||||||
|
serverId: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DatabaseConversation {
|
export interface DatabaseConversation {
|
||||||
currNode: string | null;
|
currNode: string | null;
|
||||||
id: string;
|
id: string;
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
/** Per-chat MCP server overrides. If not set, global settings are used. */
|
||||||
|
mcpServerOverrides?: McpServerOverride[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseMessageExtraAudioFile {
|
export interface DatabaseMessageExtraAudioFile {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue