refactor: MCP state management + stores/clients relationship

This commit is contained in:
Aleksander Grygier 2026-01-12 14:17:06 +01:00
parent 9c53bd4486
commit 58ab834b18
10 changed files with 225 additions and 246 deletions

View File

@ -36,10 +36,9 @@ import type {
} from '$lib/types/mcp';
import type { McpServerOverride } from '$lib/types/database';
import { MCPError } from '$lib/errors';
import { buildMcpClientConfig, incrementMcpServerUsage } from '$lib/utils/mcp';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { buildMcpClientConfig, detectMcpTransportFromUrl } from '$lib/utils/mcp';
import { config } from '$lib/stores/settings.svelte';
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
import { detectMcpTransportFromUrl } from '$lib/utils/mcp';
export type HealthCheckState =
| { status: 'idle' }
@ -68,6 +67,7 @@ export class MCPClient {
}) => void;
private onHealthCheckChange?: (serverId: string, state: HealthCheckState) => void;
private onServerUsage?: (serverId: string) => void;
/**
*
@ -99,6 +99,13 @@ export class MCPClient {
this.onHealthCheckChange = callback;
}
/**
* Set callback for server usage tracking
*/
setServerUsageCallback(callback: (serverId: string) => void): void {
this.onServerUsage = callback;
}
private notifyStateChange(state: Parameters<NonNullable<typeof this.onStateChange>>[0]): void {
this.onStateChange?.(state);
}
@ -435,8 +442,7 @@ export class MCPClient {
throw new MCPError(`Server "${serverName}" is not connected`, -32000);
}
const updatedStats = incrementMcpServerUsage(config(), serverName);
settingsStore.updateConfig('mcpServerUsageStats', updatedStats);
this.onServerUsage?.(serverName);
const args = this.parseToolArguments(toolCall.function.arguments);
return MCPService.callTool(connection, { name: toolName, arguments: args }, signal);

View File

@ -1,12 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import {
chatStore,
pendingEditMessageId,
clearPendingEditMessageId,
removeSystemPromptPlaceholder
} from '$lib/stores/chat.svelte';
import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { DatabaseService } from '$lib/services';
import { config } from '$lib/stores/settings.svelte';
@ -86,7 +81,7 @@
if (pendingId && pendingId === message.id && !isEditing) {
handleEdit();
clearPendingEditMessageId();
chatStore.clearPendingEditMessageId();
}
});
@ -95,7 +90,7 @@
// If canceling a new system message with placeholder content, remove it without deleting children
if (message.role === MessageRole.SYSTEM) {
const conversationDeleted = await removeSystemPromptPlaceholder(message.id);
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
if (conversationDeleted) {
goto(`${base}/`);
@ -187,7 +182,7 @@
// If content is empty, remove without deleting children
if (!newContent) {
const conversationDeleted = await removeSystemPromptPlaceholder(message.id);
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
isEditing = false;
if (conversationDeleted) {
goto(`${base}/`);

View File

@ -8,7 +8,7 @@
import { AttachmentType, FileTypeCategory, MimeTypeText } from '$lib/enums';
import { config } from '$lib/stores/settings.svelte';
import { useModelChangeValidation } from '$lib/hooks/use-model-change-validation.svelte';
import { setEditModeActive, clearEditMode } from '$lib/stores/chat.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { modelsStore } from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
@ -266,10 +266,10 @@
});
$effect(() => {
setEditModeActive(processNewFiles);
chatStore.setEditModeActive(processNewFiles);
return () => {
clearEditMode();
chatStore.clearEditMode();
};
});
</script>

View File

@ -1,6 +1,5 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { Button } from '$lib/components/ui/button';
import { McpLogo, McpSettingsSection } from '$lib/components/app';
@ -11,38 +10,9 @@
let { onOpenChange, open = $bindable(false) }: Props = $props();
let localConfig = $state(config());
function handleClose() {
onOpenChange?.(false);
}
function handleConfigChange(key: string, value: string | boolean) {
localConfig = { ...localConfig, [key]: value };
}
function handleSave() {
// Save all changes to settingsStore
Object.entries(localConfig).forEach(([key, value]) => {
if (config()[key as keyof typeof localConfig] !== value) {
settingsStore.updateConfig(key as keyof typeof localConfig, value);
}
});
onOpenChange?.(false);
}
function handleCancel() {
// Reset to current config
localConfig = config();
onOpenChange?.(false);
}
$effect(() => {
if (open) {
// Reset local config when dialog opens
localConfig = config();
}
});
</script>
<Dialog.Root {open} onOpenChange={handleClose}>
@ -63,12 +33,11 @@
</div>
<div class="flex-1 overflow-y-auto p-4 md:p-6">
<McpSettingsSection {localConfig} onConfigChange={handleConfigChange} />
<McpSettingsSection />
</div>
<div class="flex items-center justify-end gap-3 border-t p-4 md:p-6">
<Button variant="outline" onclick={handleCancel}>Cancel</Button>
<Button onclick={handleSave}>Save Changes</Button>
<Button onclick={handleClose}>Close</Button>
</div>
</Dialog.Content>
</Dialog.Root>

View File

@ -8,14 +8,14 @@
import McpLogo from '$lib/components/app/misc/McpLogo.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { parseMcpServerSettings, parseMcpServerUsageStats } from '$lib/utils/mcp';
import { parseMcpServerSettings, getServerDisplayName, getFaviconUrl } from '$lib/utils/mcp';
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
import {
mcpGetHealthCheckState,
mcpHasHealthCheck,
mcpRunHealthCheck
mcpGetUsageStats
} from '$lib/stores/mcp.svelte';
import { extractServerNameFromUrl, getFaviconUrl } from '$lib/utils/mcp';
import { mcpClient } from '$lib/clients/mcp.client';
interface Props {
class?: string;
@ -33,7 +33,7 @@
let hasMcpServers = $derived(mcpServers.length > 0);
let mcpUsageStats = $derived(parseMcpServerUsageStats(settingsStore.config.mcpServerUsageStats));
let mcpUsageStats = $derived(mcpGetUsageStats());
function getServerUsageCount(serverId: string): number {
return mcpUsageStats[serverId] || 0;
@ -94,11 +94,6 @@
await conversationsStore.toggleMcpServerForChat(serverId, globalEnabled);
}
function getServerDisplayName(server: MCPServerSettingsEntry): string {
if (server.name) return server.name;
return extractServerNameFromUrl(server.url);
}
let mcpFavicons = $derived(
healthyEnabledMcpServers
.slice(0, 3)
@ -111,7 +106,7 @@
onMount(() => {
for (const server of serversWithUrls) {
if (!mcpHasHealthCheck(server.id)) {
mcpRunHealthCheck(server);
mcpClient.runHealthCheck(server);
}
}
});

View File

@ -5,9 +5,9 @@
import {
mcpGetHealthCheckState,
mcpHasHealthCheck,
mcpRunHealthCheck,
type HealthCheckState
} from '$lib/stores/mcp.svelte';
import { mcpClient } from '$lib/clients/mcp.client';
import McpServerCardHeader from './McpServerCardHeader.svelte';
import McpServerCardActions from './McpServerCardActions.svelte';
import McpServerCardToolsList from './McpServerCardToolsList.svelte';
@ -38,12 +38,12 @@
onMount(() => {
if (!mcpHasHealthCheck(server.id) && server.enabled && server.url.trim()) {
mcpRunHealthCheck(server);
mcpClient.runHealthCheck(server);
}
});
function handleHealthCheck() {
mcpRunHealthCheck(server);
mcpClient.runHealthCheck(server);
}
function startEditing() {
@ -67,7 +67,7 @@
isEditing = false;
if (server.enabled && url) {
setTimeout(() => mcpRunHealthCheck({ ...server, url }), 100);
setTimeout(() => mcpClient.runHealthCheck({ ...server, url }), 100);
}
}

View File

@ -2,23 +2,14 @@
import { Plus, X } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { parseMcpServerSettings } from '$lib/utils/mcp';
import { getServerDisplayName, getFaviconUrl } from '$lib/utils/mcp';
import type { MCPServerSettingsEntry } from '$lib/types/mcp';
import type { SettingsConfigType } from '$lib/types/settings';
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
import { extractServerNameFromUrl, getFaviconUrl } from '$lib/utils/mcp';
import { mcpStore, mcpGetServers } from '$lib/stores/mcp.svelte';
import { McpServerCard } from '$lib/components/app/mcp/McpServerCard';
import McpServerForm from './McpServerForm.svelte';
interface Props {
localConfig: SettingsConfigType;
onConfigChange: (key: string, value: string | boolean) => void;
}
let { localConfig, onConfigChange }: Props = $props();
// Get servers from localConfig
let servers = $derived<MCPServerSettingsEntry[]>(parseMcpServerSettings(localConfig.mcpServers));
// Get servers from store
let servers = $derived<MCPServerSettingsEntry[]>(mcpGetServers());
// New server form state
let isAddingServer = $state(false);
@ -36,10 +27,6 @@
}
});
function serializeServers(updatedServers: MCPServerSettingsEntry[]) {
onConfigChange('mcpServers', JSON.stringify(updatedServers));
}
function showAddServerForm() {
isAddingServer = true;
newServerUrl = '';
@ -54,35 +41,15 @@
function saveNewServer() {
if (newServerUrlError) return;
const newServer: MCPServerSettingsEntry = {
id: crypto.randomUUID ? crypto.randomUUID() : `server-${Date.now()}`,
mcpStore.addServer({
enabled: true,
url: newServerUrl.trim(),
headers: newServerHeaders.trim() || undefined,
requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds
};
serializeServers([...servers, newServer]);
headers: newServerHeaders.trim() || undefined
});
isAddingServer = false;
newServerUrl = '';
newServerHeaders = '';
}
function updateServer(id: string, updates: Partial<MCPServerSettingsEntry>) {
const nextServers = servers.map((server) =>
server.id === id ? { ...server, ...updates } : server
);
serializeServers(nextServers);
}
function removeServer(id: string) {
serializeServers(servers.filter((server) => server.id !== id));
}
// Get display name for server
function getServerDisplayName(server: MCPServerSettingsEntry): string {
if (server.name) return server.name;
return extractServerNameFromUrl(server.url);
}
</script>
<div class="space-y-4">
@ -154,9 +121,9 @@
{server}
displayName={getServerDisplayName(server)}
faviconUrl={getFaviconUrl(server.url)}
onToggle={(enabled) => updateServer(server.id, { enabled })}
onUpdate={(updates) => updateServer(server.id, updates)}
onDelete={() => removeServer(server.id)}
onToggle={(enabled) => mcpStore.updateServer(server.id, { enabled })}
onUpdate={(updates) => mcpStore.updateServer(server.id, updates)}
onDelete={() => mcpStore.removeServer(server.id)}
/>
{/each}
</div>

View File

@ -204,6 +204,10 @@ class ChatStore {
return this.addFilesHandler;
}
clearPendingEditMessageId(): void {
this.pendingEditMessageId = null;
}
getAllLoadingChats(): string[] {
return Array.from(this.chatLoadingStates.keys());
}
@ -333,8 +337,8 @@ class ChatStore {
export const chatStore = new ChatStore();
// State access functions (getters only - use chatStore.method() for actions)
export const activeProcessingState = () => chatStore.activeProcessingState;
export const clearEditMode = () => chatStore.clearEditMode();
export const currentResponse = () => chatStore.currentResponse;
export const errorDialog = () => chatStore.errorDialogState;
export const getAddFilesHandler = () => chatStore.getAddFilesHandler();
@ -345,9 +349,4 @@ export const isChatLoading = (convId: string) => chatStore.isChatLoadingPublic(c
export const isChatStreaming = () => chatStore.isStreaming();
export const isEditing = () => chatStore.isEditing();
export const isLoading = () => chatStore.isLoading;
export const setEditModeActive = (handler: (files: File[]) => void) =>
chatStore.setEditModeActive(handler);
export const pendingEditMessageId = () => chatStore.pendingEditMessageId;
export const clearPendingEditMessageId = () => (chatStore.pendingEditMessageId = null);
export const removeSystemPromptPlaceholder = (messageId: string) =>
chatStore.removeSystemPromptPlaceholder(messageId);

View File

@ -20,16 +20,37 @@
*/
import { browser } from '$app/environment';
import { mcpClient, type HealthCheckState, type HealthCheckParams } from '$lib/clients';
import type {
OpenAIToolDefinition,
ServerStatus,
ToolExecutionResult,
MCPToolCall
} from '$lib/types/mcp';
import { mcpClient, type HealthCheckState } from '$lib/clients';
import type { MCPServerSettingsEntry, McpServerUsageStats } from '$lib/types/mcp';
import type { McpServerOverride } from '$lib/types/database';
import { buildMcpClientConfig } from '$lib/utils/mcp';
import { config } from '$lib/stores/settings.svelte';
import { buildMcpClientConfig, parseMcpServerSettings } from '$lib/utils/mcp';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
/**
* Parses MCP server usage stats from settings.
* @param rawStats - The raw stats to parse
* @returns MCP server usage stats or empty object if invalid
*/
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 {};
}
export type { HealthCheckState };
@ -60,6 +81,10 @@ class MCPStore {
mcpClient.setHealthCheckCallback((serverId, state) => {
this._healthChecks = { ...this._healthChecks, [serverId]: state };
});
mcpClient.setServerUsageCallback((serverId) => {
this.incrementServerUsage(serverId);
});
}
}
@ -104,67 +129,6 @@ class MCPStore {
return mcpClient.getToolNames();
}
/**
* Ensure MCP is initialized with current config.
* @param perChatOverrides - Optional per-chat MCP server overrides
*/
async ensureInitialized(perChatOverrides?: McpServerOverride[]): Promise<boolean> {
return mcpClient.ensureInitialized(perChatOverrides);
}
/**
* Shutdown MCP connections and clear state
*/
async shutdown(): Promise<void> {
return mcpClient.shutdown();
}
/**
* Get tool definitions for LLM (OpenAI function calling format)
*/
getToolDefinitions(): OpenAIToolDefinition[] {
return mcpClient.getToolDefinitionsForLLM();
}
/**
* Get status of all servers
*/
getServersStatus(): ServerStatus[] {
return mcpClient.getServersStatus();
}
/**
* Execute a tool call via MCP.
*/
async executeTool(toolCall: MCPToolCall, signal?: AbortSignal): Promise<ToolExecutionResult> {
return mcpClient.executeTool(toolCall, signal);
}
/**
* Execute a tool by name with arguments.
*/
async executeToolByName(
toolName: string,
args: Record<string, unknown>,
signal?: AbortSignal
): Promise<ToolExecutionResult> {
return mcpClient.executeToolByName(toolName, args, signal);
}
/**
* Check if a tool exists
*/
hasTool(toolName: string): boolean {
return mcpClient.hasTool(toolName);
}
/**
* Get which server provides a specific tool
*/
getToolServer(toolName: string): string | undefined {
return mcpClient.getToolServer(toolName);
}
/**
* Get health check state for a specific server
*/
@ -179,13 +143,6 @@ class MCPStore {
return serverId in this._healthChecks && this._healthChecks[serverId].status !== 'idle';
}
/**
* Run health check for a specific server
*/
async runHealthCheck(server: HealthCheckParams): Promise<void> {
return mcpClient.runHealthCheck(server);
}
/**
* Clear health check state for a specific server
*/
@ -208,6 +165,107 @@ class MCPStore {
clearError(): void {
this._error = null;
}
/**
*
* Server Management (CRUD)
*
*/
/**
* Get all configured MCP servers from settings
*/
getServers(): MCPServerSettingsEntry[] {
return parseMcpServerSettings(config().mcpServers);
}
/**
* Add a new MCP server
*/
addServer(
serverData: Omit<MCPServerSettingsEntry, 'id' | 'requestTimeoutSeconds'> & { id?: string }
): void {
const servers = this.getServers();
const newServer: MCPServerSettingsEntry = {
id: serverData.id || (crypto.randomUUID ? crypto.randomUUID() : `server-${Date.now()}`),
enabled: serverData.enabled,
url: serverData.url.trim(),
name: serverData.name,
headers: serverData.headers?.trim() || undefined,
requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds
};
settingsStore.updateConfig('mcpServers', JSON.stringify([...servers, newServer]));
}
/**
* Update an existing MCP server
*/
updateServer(id: string, updates: Partial<MCPServerSettingsEntry>): void {
const servers = this.getServers();
const nextServers = servers.map((server) =>
server.id === id ? { ...server, ...updates } : server
);
settingsStore.updateConfig('mcpServers', JSON.stringify(nextServers));
}
/**
* Remove an MCP server by ID
*/
removeServer(id: string): void {
const servers = this.getServers();
settingsStore.updateConfig('mcpServers', JSON.stringify(servers.filter((s) => s.id !== id)));
this.clearHealthCheck(id);
}
/**
* Check if a server is enabled considering per-chat overrides
*/
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;
}
/**
* Check if there are any enabled MCP servers
*/
hasEnabledServers(perChatOverrides?: McpServerOverride[]): boolean {
return Boolean(buildMcpClientConfig(config(), perChatOverrides));
}
/**
*
* Server Usage Stats
*
*/
/**
* Get parsed usage stats for all servers
*/
getUsageStats(): McpServerUsageStats {
return parseMcpServerUsageStats(config().mcpServerUsageStats);
}
/**
* Get usage count for a specific server
*/
getServerUsageCount(serverId: string): number {
const stats = this.getUsageStats();
return stats[serverId] || 0;
}
/**
* Increment usage count for a server
*/
incrementServerUsage(serverId: string): void {
const stats = this.getUsageStats();
stats[serverId] = (stats[serverId] || 0) + 1;
settingsStore.updateConfig('mcpServerUsageStats', JSON.stringify(stats));
}
}
export const mcpStore = new MCPStore();
@ -244,6 +302,7 @@ export function mcpToolCount() {
return mcpStore.toolCount;
}
// State access functions (getters only - use mcpStore.method() for actions)
export function mcpGetHealthCheckState(serverId: string) {
return mcpStore.getHealthCheckState(serverId);
}
@ -252,10 +311,25 @@ export function mcpHasHealthCheck(serverId: string) {
return mcpStore.hasHealthCheck(serverId);
}
export async function mcpRunHealthCheck(server: HealthCheckParams) {
return mcpStore.runHealthCheck(server);
export function mcpGetServers() {
return mcpStore.getServers();
}
export function mcpClearHealthCheck(serverId: string) {
return mcpStore.clearHealthCheck(serverId);
export function mcpIsServerEnabled(
server: MCPServerSettingsEntry,
perChatOverrides?: McpServerOverride[]
) {
return mcpStore.isServerEnabled(server, perChatOverrides);
}
export function mcpHasEnabledServers(perChatOverrides?: McpServerOverride[]) {
return mcpStore.hasEnabledServers(perChatOverrides);
}
export function mcpGetUsageStats() {
return mcpStore.getUsageStats();
}
export function mcpGetServerUsageCount(serverId: string) {
return mcpStore.getServerUsageCount(serverId);
}

View File

@ -2,8 +2,7 @@ import type {
MCPTransportType,
MCPClientConfig,
MCPServerConfig,
MCPServerSettingsEntry,
McpServerUsageStats
MCPServerSettingsEntry
} from '$lib/types/mcp';
import type { SettingsConfigType } from '$lib/types/settings';
import type { McpServerOverride } from '$lib/types/database';
@ -21,6 +20,7 @@ export type HeaderPair = { key: string; value: string };
*/
export function detectMcpTransportFromUrl(url: string): MCPTransportType {
const normalized = url.trim().toLowerCase();
return normalized.startsWith('ws://') || normalized.startsWith('wss://')
? 'websocket'
: 'streamable_http';
@ -34,6 +34,7 @@ export function generateMcpServerId(id: unknown, index: number): string {
if (typeof id === 'string' && id.trim()) {
return id.trim();
}
return `server-${index + 1}`;
}
@ -46,12 +47,23 @@ export function extractServerNameFromUrl(url: string): string {
const parsedUrl = new URL(url);
const host = parsedUrl.hostname.replace(/^(www\.|mcp\.)/, '');
const name = host.split('.')[0] || 'Unknown';
return name.charAt(0).toUpperCase() + name.slice(1);
} catch {
return 'New Server';
}
}
/**
* Gets a display name for an MCP server.
* Returns server.name if set, otherwise extracts name from URL.
*/
export function getServerDisplayName(server: MCPServerSettingsEntry): string {
if (server.name) return server.name;
return extractServerNameFromUrl(server.url);
}
/**
* Gets a favicon URL for an MCP server using Google's favicon service.
* Returns null if the URL is invalid.
@ -74,6 +86,7 @@ export function getFaviconUrl(serverUrl: string): string | null {
*/
export function parseHeadersToArray(headersJson: string): HeaderPair[] {
if (!headersJson?.trim()) return [];
try {
const parsed = JSON.parse(headersJson);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
@ -85,6 +98,7 @@ export function parseHeadersToArray(headersJson: string): HeaderPair[] {
} catch {
return [];
}
return [];
}
@ -94,11 +108,15 @@ export function parseHeadersToArray(headersJson: string): HeaderPair[] {
*/
export function serializeHeaders(pairs: HeaderPair[]): string {
const validPairs = pairs.filter((p) => p.key.trim());
if (validPairs.length === 0) return '';
const obj: Record<string, string> = {};
for (const pair of validPairs) {
obj[pair.key.trim()] = pair.value;
}
return JSON.stringify(obj);
}
@ -186,7 +204,8 @@ function buildServerConfig(
}
/**
* TODO - move stateful logic to store
* Checks if a server is enabled considering per-chat overrides.
* Per-chat override takes precedence over global setting.
*/
function isServerEnabled(
server: MCPServerSettingsEntry,
@ -198,6 +217,7 @@ function isServerEnabled(
return override.enabled;
}
}
return server.enabled;
}
@ -241,7 +261,9 @@ export function buildMcpClientConfig(
}
/**
* TODO - move stateful logic to store
* Checks if there are any enabled MCP servers in the configuration.
* @param config - Global settings configuration
* @param perChatOverrides - Optional per-chat server overrides
*/
export function hasEnabledMcpServers(
config: SettingsConfigType,
@ -249,51 +271,3 @@ export function hasEnabledMcpServers(
): boolean {
return Boolean(buildMcpClientConfig(config, perChatOverrides));
}
/**
* Parses MCP server usage stats from settings.
* @param rawStats - The raw stats to parse
* @returns MCP server usage stats or empty object if invalid
*/
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 {};
}
/**
* Gets usage count for a specific server.
* @param config - Global settings configuration
* @param serverId - The server ID to get the usage count for
* @returns The usage count for the server
*/
export function getMcpServerUsageCount(config: SettingsConfigType, serverId: string): number {
const stats = parseMcpServerUsageStats(config.mcpServerUsageStats);
return stats[serverId] || 0;
}
/**
* Increments usage count for a server and returns updated stats JSON.
* @param config - Global settings configuration
* @param serverId - The server ID to increment the usage count for
* @returns The 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);
}