diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte
index 40f221b0b0..74a943af22 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte
@@ -152,6 +152,19 @@
);
let canSubmit = $derived(value.trim().length > 0 || hasAttachments);
+ /**
+ *
+ *
+ * LIFECYCLE
+ *
+ *
+ */
+
+ onMount(() => {
+ recordingSupported = isAudioRecordingSupported();
+ audioRecorder = new AudioRecorder();
+ });
+
/**
*
*
@@ -184,6 +197,14 @@
return true;
}
+ /**
+ *
+ *
+ * EVENT HANDLERS - File Management
+ *
+ *
+ */
+
/**
*
*
@@ -445,19 +466,6 @@
}
}
}
-
- /**
- *
- *
- * LIFECYCLE
- *
- *
- */
-
- onMount(() => {
- recordingSupported = isAudioRecordingSupported();
- audioRecorder = new AudioRecorder();
- });
Reading (prompt processing)
{isGenerationDisabled @@ -150,6 +154,7 @@
p.length > 0);
} catch {
return [uri];
@@ -19,6 +20,7 @@ export function parseResourcePath(uri: string): string[] {
export function getDisplayName(pathPart: string): string {
const withoutExt = pathPart.replace(/\.[^.]+$/, '');
+
return withoutExt
.split(/[-_]/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
@@ -58,6 +60,7 @@ export function buildResourceTree(
children: new Map()
});
}
+
return root;
}
@@ -113,9 +116,11 @@ export function buildResourceTree(
export function countTreeResources(node: ResourceTreeNode): number {
if (node.resource) return 1;
let count = 0;
+
for (const child of node.children.values()) {
count += countTreeResources(child);
}
+
return count;
}
@@ -126,6 +131,7 @@ export function getResourceIcon(resource: MCPResourceInfo) {
if (mimeType.startsWith('image/') || /\.(png|jpg|jpeg|gif|svg|webp)$/.test(uri)) {
return Image;
}
+
if (
mimeType.includes('json') ||
mimeType.includes('javascript') ||
@@ -134,12 +140,15 @@ export function getResourceIcon(resource: MCPResourceInfo) {
) {
return Code;
}
+
if (mimeType.includes('text') || /\.(txt|md|log)$/.test(uri)) {
return FileText;
}
+
if (uri.includes('database') || uri.includes('db://')) {
return Database;
}
+
return File;
}
@@ -147,8 +156,10 @@ export function sortTreeChildren(children: ResourceTreeNode[]): ResourceTreeNode
return children.sort((a, b) => {
const aIsFolder = !a.resource && a.children.size > 0;
const bIsFolder = !b.resource && b.children.size > 0;
+
if (aIsFolder && !bIsFolder) return -1;
if (!aIsFolder && bIsFolder) return 1;
+
return a.name.localeCompare(b.name);
});
}
diff --git a/tools/server/webui/src/lib/constants/mcp-resource.ts b/tools/server/webui/src/lib/constants/mcp-resource.ts
index 4ccf1ee333..8f82d1d2c5 100644
--- a/tools/server/webui/src/lib/constants/mcp-resource.ts
+++ b/tools/server/webui/src/lib/constants/mcp-resource.ts
@@ -1,4 +1,25 @@
+import { MimeTypeImage } from '$lib/enums';
+
// File extension patterns for resource type detection
-export const IMAGE_FILE_EXTENSION_REGEX = /\.(png|jpg|jpeg|gif|svg|webp)$/;
-export const CODE_FILE_EXTENSION_REGEX = /\.(js|ts|json|yaml|yml|xml|html|css)$/;
-export const TEXT_FILE_EXTENSION_REGEX = /\.(txt|md|log)$/;
+export const IMAGE_FILE_EXTENSION_REGEX = /\.(png|jpg|jpeg|gif|svg|webp)$/i;
+export const CODE_FILE_EXTENSION_REGEX =
+ /\.(js|ts|json|yaml|yml|xml|html|css|py|rs|go|java|cpp|c|h|rb|sh|toml)$/i;
+export const TEXT_FILE_EXTENSION_REGEX = /\.(txt|md|log)$/i;
+
+// URI protocol prefix pattern
+export const PROTOCOL_PREFIX_REGEX = /^[a-z]+:\/\//;
+
+// File extension regex for display name extraction
+export const FILE_EXTENSION_REGEX = /\.[^.]+$/;
+
+/**
+ * Mapping from image MIME types to file extensions.
+ * Used for generating attachment filenames from MIME types.
+ */
+export const IMAGE_MIME_TO_EXTENSION: Record = {
+ [MimeTypeImage.JPEG]: 'jpg',
+ 'image/jpg': 'jpg',
+ [MimeTypeImage.PNG]: 'png',
+ [MimeTypeImage.GIF]: 'gif',
+ [MimeTypeImage.WEBP]: 'webp'
+} as const;
diff --git a/tools/server/webui/src/lib/services/chat.service.ts b/tools/server/webui/src/lib/services/chat.service.ts
index 4c329df73c..49c39e4ddd 100644
--- a/tools/server/webui/src/lib/services/chat.service.ts
+++ b/tools/server/webui/src/lib/services/chat.service.ts
@@ -146,6 +146,7 @@ export class ChatService {
.map((msg) => {
if ('id' in msg && 'convId' in msg && 'timestamp' in msg) {
const dbMsg = msg as DatabaseMessage & { extra?: DatabaseMessageExtra[] };
+
return ChatService.convertDbMessageToApiChatMessageData(dbMsg);
} else {
return msg as ApiChatMessageData;
@@ -171,8 +172,10 @@ export class ChatService {
console.info(
`[ChatService] Skipping image attachment in message history (model "${options.model}" does not support vision)`
);
+
return false;
}
+
return true;
});
// If only text remains and it's a single part, simplify to string
@@ -260,9 +263,11 @@ export class ChatService {
if (!response.ok) {
const error = await ChatService.parseErrorResponse(response);
+
if (onError) {
onError(error);
}
+
throw error;
}
@@ -279,6 +284,7 @@ export class ChatService {
conversationId,
signal
);
+
return;
} else {
return ChatService.handleNonStreamResponse(
@@ -317,9 +323,11 @@ export class ChatService {
}
console.error('Error in sendMessage:', error);
+
if (onError) {
onError(userFriendlyError);
}
+
throw userFriendlyError;
}
}
@@ -438,6 +446,7 @@ export class ChatService {
const data = line.slice(6);
if (data === '[DONE]') {
streamFinished = true;
+
continue;
}
@@ -543,6 +552,7 @@ export class ChatService {
if (!responseText.trim()) {
const noResponseError = new Error('No response received from server. Please try again.');
+
throw noResponseError;
}
@@ -572,6 +582,7 @@ export class ChatService {
if (!content.trim() && !serializedToolCalls) {
const noResponseError = new Error('No response received from server. Please try again.');
+
throw noResponseError;
}
@@ -691,9 +702,11 @@ export class ChatService {
role: message.role as MessageRole,
content: message.content
};
+
if (toolCalls && toolCalls.length > 0) {
result.tool_calls = toolCalls;
}
+
return result;
}
@@ -865,6 +878,7 @@ export class ChatService {
contextInfo?: { n_prompt_tokens: number; n_ctx: number };
};
fallback.name = 'HttpError';
+
return fallback;
}
}
@@ -896,18 +910,26 @@ export class ChatService {
// 1) root (some implementations provide `model` at the top level)
const rootModel = getTrimmedString(root.model);
- if (rootModel) return rootModel;
+ if (rootModel) {
+ return rootModel;
+ }
// 2) streaming choice (delta) or final response (message)
const firstChoice = Array.isArray(root.choices) ? asRecord(root.choices[0]) : undefined;
- if (!firstChoice) return undefined;
+ if (!firstChoice) {
+ return undefined;
+ }
// priority: delta.model (first chunk) else message.model (final response)
const deltaModel = getTrimmedString(asRecord(firstChoice.delta)?.model);
- if (deltaModel) return deltaModel;
+ if (deltaModel) {
+ return deltaModel;
+ }
const messageModel = getTrimmedString(asRecord(firstChoice.message)?.model);
- if (messageModel) return messageModel;
+ if (messageModel) {
+ return messageModel;
+ }
// avoid guessing from non-standard locations (metadata, etc.)
return undefined;
diff --git a/tools/server/webui/src/lib/services/mcp.service.ts b/tools/server/webui/src/lib/services/mcp.service.ts
index 4359541b52..883953b5fe 100644
--- a/tools/server/webui/src/lib/services/mcp.service.ts
+++ b/tools/server/webui/src/lib/services/mcp.service.ts
@@ -159,6 +159,7 @@ export class MCPService {
} catch (sseError) {
const httpMsg = httpError instanceof Error ? httpError.message : String(httpError);
const sseMsg = sseError instanceof Error ? sseError.message : String(sseError);
+
throw new Error(`Failed to create transport. StreamableHTTP: ${httpMsg}; SSE: ${sseMsg}`);
}
}
@@ -168,7 +169,10 @@ export class MCPService {
* Extract server info from SDK Implementation type
*/
private static extractServerInfo(impl: Implementation | undefined): MCPServerInfo | undefined {
- if (!impl) return undefined;
+ if (!impl) {
+ return undefined;
+ }
+
return {
name: impl.name,
version: impl.version,
@@ -213,6 +217,7 @@ export class MCPService {
if (import.meta.env.DEV) {
console.log(`[MCPService][${serverName}] Creating transport...`);
}
+
const { transport, type: transportType } = this.createTransport(serverConfig);
// Setup WebSocket reconnection handler
@@ -334,6 +339,7 @@ export class MCPService {
if (connection.transport.onclose) {
connection.transport.onclose = undefined;
}
+
await connection.client.close();
} catch (error) {
console.warn(`[MCPService][${connection.serverName}] Error during disconnect:`, error);
@@ -346,6 +352,7 @@ export class MCPService {
static async listTools(connection: MCPConnection): Promise {
try {
const result = await connection.client.listTools();
+
return result.tools ?? [];
} catch (error) {
console.warn(`[MCPService][${connection.serverName}] Failed to list tools:`, error);
@@ -360,6 +367,7 @@ export class MCPService {
static async listPrompts(connection: MCPConnection): Promise {
try {
const result = await connection.client.listPrompts();
+
return result.prompts ?? [];
} catch (error) {
console.warn(`[MCPService][${connection.serverName}] Failed to list prompts:`, error);
diff --git a/tools/server/webui/src/lib/stores/agentic.svelte.ts b/tools/server/webui/src/lib/stores/agentic.svelte.ts
index d9e36a80f9..5eb6dd0262 100644
--- a/tools/server/webui/src/lib/stores/agentic.svelte.ts
+++ b/tools/server/webui/src/lib/stores/agentic.svelte.ts
@@ -119,13 +119,16 @@ class AgenticStore {
}
return session;
}
+
private updateSession(conversationId: string, update: Partial): void {
const session = this.getSession(conversationId);
this._sessions.set(conversationId, { ...session, ...update });
}
+
clearSession(conversationId: string): void {
this._sessions.delete(conversationId);
}
+
getActiveSessions(): Array<{ conversationId: string; session: AgenticSession }> {
const active: Array<{ conversationId: string; session: AgenticSession }> = [];
for (const [conversationId, session] of this._sessions.entries()) {
@@ -137,18 +140,23 @@ class AgenticStore {
isRunning(conversationId: string): boolean {
return this.getSession(conversationId).isRunning;
}
+
currentTurn(conversationId: string): number {
return this.getSession(conversationId).currentTurn;
}
+
totalToolCalls(conversationId: string): number {
return this.getSession(conversationId).totalToolCalls;
}
+
lastError(conversationId: string): Error | null {
return this.getSession(conversationId).lastError;
}
+
streamingToolCall(conversationId: string): { name: string; arguments: string } | null {
return this.getSession(conversationId).streamingToolCall;
}
+
clearError(conversationId: string): void {
this.updateSession(conversationId, { lastError: null });
}
@@ -412,6 +420,7 @@ class AgenticStore {
this.buildFinalTimings(capturedTimings, agenticTimings),
undefined
);
+
return;
}
const normalizedError = error instanceof Error ? error : new Error('LLM stream error');
@@ -422,6 +431,7 @@ class AgenticStore {
this.buildFinalTimings(capturedTimings, agenticTimings),
undefined
);
+
throw normalizedError;
}
@@ -432,6 +442,7 @@ class AgenticStore {
this.buildFinalTimings(capturedTimings, agenticTimings),
undefined
);
+
return;
}
@@ -453,6 +464,7 @@ class AgenticStore {
function: call.function ? { ...call.function } : undefined
});
}
+
this.updateSession(conversationId, { totalToolCalls: allToolCalls.length });
onToolCallChunk?.(JSON.stringify(allToolCalls));
@@ -470,6 +482,7 @@ class AgenticStore {
this.buildFinalTimings(capturedTimings, agenticTimings),
undefined
);
+
return;
}
@@ -493,6 +506,7 @@ class AgenticStore {
this.buildFinalTimings(capturedTimings, agenticTimings),
undefined
);
+
return;
}
result = `Error: ${error instanceof Error ? error.message : String(error)}`;
@@ -519,6 +533,7 @@ class AgenticStore {
this.buildFinalTimings(capturedTimings, agenticTimings),
undefined
);
+
return;
}
@@ -588,10 +603,14 @@ class AgenticStore {
maxLines: number,
emit?: (chunk: string) => void
): void {
- if (!emit) return;
+ if (!emit) {
+ return;
+ }
+
let output = `\n${AGENTIC_TAGS.TOOL_ARGS_END}`;
const lines = result.split('\n');
const trimmedLines = lines.length > maxLines ? lines.slice(-maxLines) : lines;
+
output += `\n${trimmedLines.join('\n')}\n${AGENTIC_TAGS.TOOL_CALL_END}\n`;
emit(output);
}
@@ -600,26 +619,38 @@ class AgenticStore {
cleanedResult: string;
attachments: DatabaseMessageExtra[];
} {
- if (!result.trim()) return { cleanedResult: result, attachments: [] };
+ if (!result.trim()) {
+ return { cleanedResult: result, attachments: [] };
+ }
+
const lines = result.split('\n');
const attachments: DatabaseMessageExtra[] = [];
let attachmentIndex = 0;
const cleanedLines = lines.map((line) => {
const trimmedLine = line.trim();
+
const match = trimmedLine.match(/^data:([^;]+);base64,([A-Za-z0-9+/]+=*)$/);
- if (!match) return line;
+ if (!match) {
+ return line;
+ }
+
const mimeType = match[1].toLowerCase();
const base64Data = match[2];
- if (!base64Data) return line;
+
+ if (!base64Data) {
+ return line;
+ }
attachmentIndex += 1;
const name = this.buildAttachmentName(mimeType, attachmentIndex);
if (mimeType.startsWith('image/')) {
attachments.push({ type: AttachmentType.IMAGE, name, base64Url: trimmedLine });
+
return `[Attachment saved: ${name}]`;
}
+
return line;
});
@@ -644,18 +675,23 @@ export const agenticStore = new AgenticStore();
export function agenticIsRunning(conversationId: string) {
return agenticStore.isRunning(conversationId);
}
+
export function agenticCurrentTurn(conversationId: string) {
return agenticStore.currentTurn(conversationId);
}
+
export function agenticTotalToolCalls(conversationId: string) {
return agenticStore.totalToolCalls(conversationId);
}
+
export function agenticLastError(conversationId: string) {
return agenticStore.lastError(conversationId);
}
+
export function agenticStreamingToolCall(conversationId: string) {
return agenticStore.streamingToolCall(conversationId);
}
+
export function agenticIsAnyRunning() {
return agenticStore.isAnyRunning;
}
diff --git a/tools/server/webui/src/lib/stores/mcp-resources.svelte.ts b/tools/server/webui/src/lib/stores/mcp-resources.svelte.ts
index 481c4491b5..f440907a41 100644
--- a/tools/server/webui/src/lib/stores/mcp-resources.svelte.ts
+++ b/tools/server/webui/src/lib/stores/mcp-resources.svelte.ts
@@ -63,6 +63,7 @@ class MCPResourceStore {
for (const serverRes of this._serverResources.values()) {
count += serverRes.resources.length;
}
+
return count;
}
@@ -71,6 +72,7 @@ class MCPResourceStore {
for (const serverRes of this._serverResources.values()) {
count += serverRes.templates.length;
}
+
return count;
}
@@ -134,6 +136,7 @@ class MCPResourceStore {
*/
setServerError(serverName: string, error: string): void {
const existing = this._serverResources.get(serverName);
+
if (existing) {
this._serverResources.set(serverName, { ...existing, loading: false, error });
} else {
@@ -159,6 +162,7 @@ class MCPResourceStore {
*/
getAllResourceInfos(): MCPResourceInfo[] {
const result: MCPResourceInfo[] = [];
+
for (const [serverName, serverRes] of this._serverResources) {
for (const resource of serverRes.resources) {
result.push({
@@ -173,6 +177,7 @@ class MCPResourceStore {
});
}
}
+
return result;
}
@@ -181,6 +186,7 @@ class MCPResourceStore {
*/
getAllTemplateInfos(): MCPResourceTemplateInfo[] {
const result: MCPResourceTemplateInfo[] = [];
+
for (const [serverName, serverRes] of this._serverResources) {
for (const template of serverRes.templates) {
result.push({
@@ -195,6 +201,7 @@ class MCPResourceStore {
});
}
}
+
return result;
}
@@ -203,18 +210,21 @@ class MCPResourceStore {
*/
clearServerResources(serverName: string): void {
this._serverResources.delete(serverName);
+
// Also clear cached content for this server's resources
for (const [uri, cached] of this._cachedResources) {
if (cached.resource.serverName === serverName) {
this._cachedResources.delete(uri);
}
}
+
// Clear subscriptions for this server
for (const [uri, sub] of this._subscriptions) {
if (sub.serverName === serverName) {
this._subscriptions.delete(uri);
}
}
+
console.log(`[MCPResources][${serverName}] Cleared all resources`);
}
@@ -260,6 +270,7 @@ class MCPResourceStore {
if (age > CACHE_TTL_MS && !cached.subscribed) {
// Cache expired and not subscribed, remove it
this._cachedResources.delete(uri);
+
return undefined;
}
@@ -468,6 +479,7 @@ class MCPResourceStore {
};
}
}
+
return undefined;
}
@@ -480,6 +492,7 @@ class MCPResourceStore {
return serverName;
}
}
+
return undefined;
}
diff --git a/tools/server/webui/src/lib/stores/mcp.svelte.ts b/tools/server/webui/src/lib/stores/mcp.svelte.ts
index 45b27f63d3..039e59007d 100644
--- a/tools/server/webui/src/lib/stores/mcp.svelte.ts
+++ b/tools/server/webui/src/lib/stores/mcp.svelte.ts
@@ -56,16 +56,25 @@ import type { McpServerOverride } from '$lib/types/database';
import type { SettingsConfigType } from '$lib/types/settings';
function generateMcpServerId(id: unknown, index: number): string {
- if (typeof id === 'string' && id.trim()) return id.trim();
+ if (typeof id === 'string' && id.trim()) {
+ return id.trim();
+ }
+
return `${MCP_SERVER_ID_PREFIX}${index + 1}`;
}
function parseServerSettings(rawServers: unknown): MCPServerSettingsEntry[] {
- if (!rawServers) return [];
+ if (!rawServers) {
+ return [];
+ }
+
let parsed: unknown;
if (typeof rawServers === 'string') {
const trimmed = rawServers.trim();
- if (!trimmed) return [];
+ if (!trimmed) {
+ return [];
+ }
+
try {
parsed = JSON.parse(trimmed);
} catch (error) {
@@ -75,7 +84,10 @@ function parseServerSettings(rawServers: unknown): MCPServerSettingsEntry[] {
} else {
parsed = rawServers;
}
- if (!Array.isArray(parsed)) return [];
+ if (!Array.isArray(parsed)) {
+ return [];
+ }
+
return parsed.map((entry, index) => {
const url = typeof entry?.url === 'string' ? entry.url.trim() : '';
const headers = typeof entry?.headers === 'string' ? entry.headers.trim() : undefined;
@@ -95,7 +107,10 @@ function buildServerConfig(
entry: MCPServerSettingsEntry,
connectionTimeoutMs = DEFAULT_MCP_CONFIG.connectionTimeoutMs
): MCPServerConfig | undefined {
- if (!entry?.url) return undefined;
+ if (!entry?.url) {
+ return undefined;
+ }
+
let headers: Record | undefined;
if (entry.headers) {
try {
@@ -120,7 +135,10 @@ function checkServerEnabled(
server: MCPServerSettingsEntry,
perChatOverrides?: McpServerOverride[]
): boolean {
- if (!server.enabled) return false;
+ if (!server.enabled) {
+ return false;
+ }
+
if (perChatOverrides) {
const override = perChatOverrides.find((o) => o.serverId === server.id);
return override?.enabled ?? false;
@@ -133,14 +151,20 @@ function buildMcpClientConfigInternal(
perChatOverrides?: McpServerOverride[]
): MCPClientConfig | undefined {
const rawServers = parseServerSettings(cfg.mcpServers);
- if (!rawServers.length) return undefined;
+ if (!rawServers.length) {
+ return undefined;
+ }
+
const servers: Record = {};
for (const [index, entry] of rawServers.entries()) {
if (!checkServerEnabled(entry, perChatOverrides)) continue;
const normalized = buildServerConfig(entry);
if (normalized) servers[generateMcpServerId(entry.id, index)] = normalized;
}
- if (Object.keys(servers).length === 0) return undefined;
+ if (Object.keys(servers).length === 0) {
+ return undefined;
+ }
+
return {
protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion,
capabilities: DEFAULT_MCP_CONFIG.capabilities,
@@ -303,13 +327,17 @@ class MCPStore {
*/
getServerFavicon(serverId: string): string | null {
const server = this.getServerById(serverId);
- if (!server) return null;
+ if (!server) {
+ return null;
+ }
+
return getFaviconUrl(server.url);
}
isAnyServerLoading(): boolean {
return this.getServers().some((s) => {
const state = this.getHealthCheckState(s.id);
+
return (
state.status === HealthCheckStatus.IDLE || state.status === HealthCheckStatus.CONNECTING
);
@@ -318,7 +346,10 @@ class MCPStore {
getServersSorted(): MCPServerSettingsEntry[] {
const servers = this.getServers();
- if (this.isAnyServerLoading()) return servers;
+ if (this.isAnyServerLoading()) {
+ return servers;
+ }
+
return [...servers].sort((a, b) =>
this.getServerLabel(a).localeCompare(this.getServerLabel(b))
);
@@ -366,24 +397,41 @@ class MCPStore {
getEnabledServersForConversation(
perChatOverrides?: McpServerOverride[]
): MCPServerSettingsEntry[] {
- if (!perChatOverrides?.length) return [];
+ if (!perChatOverrides?.length) {
+ return [];
+ }
+
return this.getServers().filter((server) => {
- if (!server.enabled) return false;
+ if (!server.enabled) {
+ return false;
+ }
+
const override = perChatOverrides.find((o) => o.serverId === server.id);
+
return override?.enabled ?? false;
});
}
async ensureInitialized(perChatOverrides?: McpServerOverride[]): Promise {
- if (!browser) return false;
+ if (!browser) {
+ return false;
+ }
+
const mcpConfig = buildMcpClientConfigInternal(config(), perChatOverrides);
const signature = mcpConfig ? JSON.stringify(mcpConfig) : null;
if (!signature) {
await this.shutdown();
+
return false;
}
- if (this.isInitialized && this.configSignature === signature) return true;
- if (this.initPromise && this.configSignature === signature) return this.initPromise;
+ if (this.isInitialized && this.configSignature === signature) {
+ return true;
+ }
+
+ if (this.initPromise && this.configSignature === signature) {
+ return this.initPromise;
+ }
+
if (this.connections.size > 0 || this.initPromise) await this.shutdown();
return this.initialize(signature, mcpConfig!);
}
@@ -391,12 +439,16 @@ class MCPStore {
private async initialize(signature: string, mcpConfig: MCPClientConfig): Promise {
this.updateState({ isInitializing: true, error: null });
this.configSignature = signature;
+
const serverEntries = Object.entries(mcpConfig.servers);
+
if (serverEntries.length === 0) {
this.updateState({ isInitializing: false, toolCount: 0, connectedServers: [] });
+
return false;
}
this.initPromise = this.doInitialize(signature, mcpConfig, serverEntries);
+
return this.initPromise;
}
@@ -427,6 +479,7 @@ class MCPStore {
},
listChangedHandlers
);
+
return { name, connection };
})
);
@@ -435,12 +488,15 @@ class MCPStore {
if (result.status === 'fulfilled')
await MCPService.disconnect(result.value.connection).catch(console.warn);
}
+
return false;
}
for (const result of results) {
if (result.status === 'fulfilled') {
const { name, connection } = result.value;
+
this.connections.set(name, connection);
+
for (const tool of connection.tools) {
if (this.toolsIndex.has(tool.name))
console.warn(
@@ -452,6 +508,7 @@ class MCPStore {
console.error(`[MCPStore] Failed to connect:`, result.reason);
}
}
+
const successCount = this.connections.size;
if (successCount === 0 && serverEntries.length > 0) {
this.updateState({
@@ -461,8 +518,10 @@ class MCPStore {
connectedServers: []
});
this.initPromise = null;
+
return false;
}
+
this.updateState({
isInitializing: false,
error: null,
@@ -470,6 +529,7 @@ class MCPStore {
connectedServers: Array.from(this.connections.keys())
});
this.initPromise = null;
+
return true;
}
@@ -497,11 +557,16 @@ class MCPStore {
private handleToolsListChanged(serverName: string, tools: Tool[]): void {
const connection = this.connections.get(serverName);
- if (!connection) return;
+ if (!connection) {
+ return;
+ }
+
for (const [toolName, ownerServer] of this.toolsIndex.entries()) {
if (ownerServer === serverName) this.toolsIndex.delete(toolName);
}
+
connection.tools = tools;
+
for (const tool of tools) {
if (this.toolsIndex.has(tool.name))
console.warn(
@@ -537,7 +602,11 @@ class MCPStore {
await this.initPromise.catch(() => {});
this.initPromise = null;
}
- if (this.connections.size === 0) return;
+
+ if (this.connections.size === 0) {
+ return;
+ }
+
await Promise.all(
Array.from(this.connections.values()).map((conn) =>
MCPService.disconnect(conn).catch((error) =>
@@ -545,6 +614,7 @@ class MCPStore {
)
)
);
+
this.connections.clear();
this.toolsIndex.clear();
this.serverConfigs.clear();
@@ -560,12 +630,14 @@ class MCPStore {
// Guard against concurrent reconnections
if (this.reconnectingServers.has(serverName)) {
console.log(`[MCPStore][${serverName}] Reconnection already in progress, skipping`);
+
return;
}
const serverConfig = this.serverConfigs.get(serverName);
if (!serverConfig) {
console.error(`[MCPStore] No config found for ${serverName}, cannot reconnect`);
+
return;
}
@@ -616,6 +688,7 @@ class MCPStore {
getToolDefinitionsForLLM(): OpenAIToolDefinition[] {
const tools: OpenAIToolDefinition[] = [];
+
for (const connection of this.connections.values()) {
for (const tool of connection.tools) {
const rawSchema = (tool.inputSchema as Record) ?? {
@@ -623,6 +696,7 @@ class MCPStore {
properties: {},
required: []
};
+
tools.push({
type: 'function' as const,
function: {
@@ -633,11 +707,15 @@ class MCPStore {
});
}
}
+
return tools;
}
private normalizeSchemaProperties(schema: Record): Record {
- if (!schema || typeof schema !== 'object') return schema;
+ if (!schema || typeof schema !== 'object') {
+ return schema;
+ }
+
const normalized = { ...schema };
if (normalized.properties && typeof normalized.properties === 'object') {
const props = normalized.properties as Record>;
@@ -671,23 +749,29 @@ class MCPStore {
}
normalized.properties = normalizedProps;
}
+
return normalized;
}
getToolNames(): string[] {
return Array.from(this.toolsIndex.keys());
}
+
hasTool(toolName: string): boolean {
return this.toolsIndex.has(toolName);
}
+
getToolServer(toolName: string): string | undefined {
return this.toolsIndex.get(toolName);
}
hasPromptsSupport(): boolean {
for (const connection of this.connections.values()) {
- if (connection.serverCapabilities?.prompts) return true;
+ if (connection.serverCapabilities?.prompts) {
+ return true;
+ }
}
+
return false;
}
@@ -705,8 +789,11 @@ class MCPStore {
const enabledServerIds = new Set(
perChatOverrides.filter((o) => o.enabled).map((o) => o.serverId)
);
+
// No enabled servers = no capability
- if (enabledServerIds.size === 0) return false;
+ if (enabledServerIds.size === 0) {
+ return false;
+ }
// Check health check states for enabled servers with prompts capability
for (const [serverId, state] of Object.entries(this._healthChecks)) {
@@ -718,6 +805,7 @@ class MCPStore {
return true;
}
}
+
// Also check active connections as fallback
for (const [serverName, connection] of this.connections) {
if (!enabledServerIds.has(serverName)) continue;
@@ -725,6 +813,7 @@ class MCPStore {
return true;
}
}
+
return false;
}
@@ -737,19 +826,24 @@ class MCPStore {
return true;
}
}
+
for (const connection of this.connections.values()) {
if (connection.serverCapabilities?.prompts) {
return true;
}
}
+
return false;
}
async getAllPrompts(): Promise {
const results: MCPPromptInfo[] = [];
+
for (const [serverName, connection] of this.connections) {
if (!connection.serverCapabilities?.prompts) continue;
+
const prompts = await MCPService.listPrompts(connection);
+
for (const prompt of prompts) {
results.push({
name: prompt.name,
@@ -764,6 +858,7 @@ class MCPStore {
});
}
}
+
return results;
}
@@ -774,16 +869,21 @@ class MCPStore {
): Promise {
const connection = this.connections.get(serverName);
if (!connection) throw new Error(`Server "${serverName}" not found for prompt "${promptName}"`);
+
return MCPService.getPrompt(connection, promptName, args);
}
async executeTool(toolCall: MCPToolCall, signal?: AbortSignal): Promise {
const toolName = toolCall.function.name;
+
const serverName = this.toolsIndex.get(toolName);
if (!serverName) throw new Error(`Unknown tool: ${toolName}`);
+
const connection = this.connections.get(serverName);
if (!connection) throw new Error(`Server "${serverName}" is not connected`);
+
const args = this.parseToolArguments(toolCall.function.arguments);
+
return MCPService.callTool(connection, { name: toolName, arguments: args }, signal);
}
@@ -796,25 +896,34 @@ class MCPStore {
if (!serverName) throw new Error(`Unknown tool: ${toolName}`);
const connection = this.connections.get(serverName);
if (!connection) throw new Error(`Server "${serverName}" is not connected`);
+
return MCPService.callTool(connection, { name: toolName, arguments: args }, signal);
}
private parseToolArguments(args: string | Record): Record {
if (typeof args === 'string') {
const trimmed = args.trim();
- if (trimmed === '') return {};
+ if (trimmed === '') {
+ return {};
+ }
+
try {
const parsed = JSON.parse(trimmed);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
throw new Error(
`Tool arguments must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`
);
+
return parsed as Record;
} catch (error) {
throw new Error(`Failed to parse tool arguments as JSON: ${(error as Error).message}`);
}
}
- if (typeof args === 'object' && args !== null && !Array.isArray(args)) return args;
+
+ if (typeof args === 'object' && args !== null && !Array.isArray(args)) {
+ return args;
+ }
+
throw new Error(`Invalid tool arguments type: ${typeof args}`);
}
@@ -829,7 +938,10 @@ class MCPStore {
console.warn(`[MCPStore] Server "${serverName}" is not connected`);
return null;
}
- if (!connection.serverCapabilities?.completions) return null;
+ if (!connection.serverCapabilities?.completions) {
+ return null;
+ }
+
return MCPService.complete(
connection,
{ type: MCPRefType.PROMPT, name: promptName },
@@ -838,7 +950,10 @@ class MCPStore {
}
private parseHeaders(headersJson?: string): Record | undefined {
- if (!headersJson?.trim()) return undefined;
+ if (!headersJson?.trim()) {
+ return undefined;
+ }
+
try {
const parsed = JSON.parse(headersJson);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed))
@@ -846,6 +961,7 @@ class MCPStore {
} catch {
console.warn('[MCPStore] Failed to parse custom headers JSON:', headersJson);
}
+
return undefined;
}
@@ -863,7 +979,11 @@ class MCPStore {
const serversToCheck = skipIfChecked
? servers.filter((s) => !this.hasHealthCheck(s.id) && s.url.trim())
: servers.filter((s) => s.url.trim());
- if (serversToCheck.length === 0) return;
+
+ if (serversToCheck.length === 0) {
+ return;
+ }
+
const BATCH_SIZE = 5;
for (let i = 0; i < serversToCheck.length; i += BATCH_SIZE) {
const batch = serversToCheck.slice(i, i + BATCH_SIZE);
@@ -921,9 +1041,11 @@ class MCPStore {
this.connections.delete(server.id);
}
}
+
const trimmedUrl = server.url.trim();
const logs: MCPConnectionLog[] = [];
let currentPhase: MCPConnectionPhase = MCPConnectionPhase.IDLE;
+
if (!trimmedUrl) {
this.updateHealthCheck(server.id, {
status: HealthCheckStatus.ERROR,
@@ -932,11 +1054,13 @@ class MCPStore {
});
return;
}
+
this.updateHealthCheck(server.id, {
status: HealthCheckStatus.CONNECTING,
phase: MCPConnectionPhase.TRANSPORT_CREATING,
logs: []
});
+
const timeoutMs = Math.round(server.requestTimeoutSeconds * 1000);
const headers = this.parseHeaders(server.headers);
@@ -976,15 +1100,18 @@ class MCPStore {
}
}
);
+
const tools = connection.tools.map((tool) => ({
name: tool.name,
description: tool.description,
title: tool.title
}));
+
const capabilities = buildCapabilitiesInfo(
connection.serverCapabilities,
connection.clientCapabilities
);
+
this.updateHealthCheck(server.id, {
status: HealthCheckStatus.SUCCESS,
tools,
@@ -1005,12 +1132,14 @@ class MCPStore {
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error occurred';
+
logs.push({
timestamp: new Date(),
phase: MCPConnectionPhase.ERROR,
message: `Connection failed: ${message}`,
level: MCPLogLevel.ERROR
});
+
this.updateHealthCheck(server.id, {
status: HealthCheckStatus.ERROR,
message,
@@ -1047,6 +1176,7 @@ class MCPStore {
getServersStatus(): ServerStatus[] {
const statuses: ServerStatus[] = [];
+
for (const [name, connection] of this.connections) {
statuses.push({
name,
@@ -1055,6 +1185,7 @@ class MCPStore {
error: undefined
});
}
+
return statuses;
}
@@ -1068,6 +1199,7 @@ class MCPStore {
instructions: string;
}> {
const results: Array<{ serverName: string; serverTitle?: string; instructions: string }> = [];
+
for (const [serverName, connection] of this.connections) {
if (connection.instructions) {
results.push({
@@ -1077,6 +1209,7 @@ class MCPStore {
});
}
}
+
return results;
}
@@ -1090,6 +1223,7 @@ class MCPStore {
instructions: string;
}> {
const results: Array<{ serverId: string; serverTitle?: string; instructions: string }> = [];
+
for (const [serverId, state] of Object.entries(this._healthChecks)) {
if (state.status === HealthCheckStatus.SUCCESS && state.instructions) {
results.push({
@@ -1099,6 +1233,7 @@ class MCPStore {
});
}
}
+
return results;
}
@@ -1107,8 +1242,11 @@ class MCPStore {
*/
hasServerInstructions(): boolean {
for (const connection of this.connections.values()) {
- if (connection.instructions) return true;
+ if (connection.instructions) {
+ return true;
+ }
}
+
return false;
}
@@ -1135,7 +1273,9 @@ class MCPStore {
perChatOverrides.filter((o) => o.enabled).map((o) => o.serverId)
);
// No enabled servers = no capability
- if (enabledServerIds.size === 0) return false;
+ if (enabledServerIds.size === 0) {
+ return false;
+ }
// Check health check states for enabled servers with resources capability
for (const [serverId, state] of Object.entries(this._healthChecks)) {
@@ -1147,6 +1287,7 @@ class MCPStore {
return true;
}
}
+
// Also check active connections as fallback
for (const [serverName, connection] of this.connections) {
if (!enabledServerIds.has(serverName)) continue;
@@ -1154,6 +1295,7 @@ class MCPStore {
return true;
}
}
+
return false;
}
@@ -1166,11 +1308,13 @@ class MCPStore {
return true;
}
}
+
for (const connection of this.connections.values()) {
if (MCPService.supportsResources(connection)) {
return true;
}
}
+
return false;
}
@@ -1217,14 +1361,19 @@ class MCPStore {
if (!forceRefresh) {
const allServersCached = serversWithResources.every((serverName) => {
const serverRes = mcpResourceStore.getServerResources(serverName);
- if (!serverRes || !serverRes.lastFetched) return false;
+ if (!serverRes || !serverRes.lastFetched) {
+ return false;
+ }
+
// Cache is valid for 5 minutes
const age = Date.now() - serverRes.lastFetched.getTime();
+
return age < 5 * 60 * 1000;
});
if (allServersCached) {
console.log('[MCPStore] Using cached resources');
+
return;
}
}
@@ -1286,12 +1435,14 @@ class MCPStore {
const serverName = mcpResourceStore.findServerForUri(uri);
if (!serverName) {
console.error(`[MCPStore] No server found for resource URI: ${uri}`);
+
return null;
}
const connection = this.connections.get(serverName);
if (!connection) {
console.error(`[MCPStore] No connection found for server: ${serverName}`);
+
return null;
}
@@ -1306,6 +1457,7 @@ class MCPStore {
return result.contents;
} catch (error) {
console.error(`[MCPStore] Failed to read resource ${uri}:`, error);
+
return null;
}
}
@@ -1317,12 +1469,14 @@ class MCPStore {
const serverName = mcpResourceStore.findServerForUri(uri);
if (!serverName) {
console.error(`[MCPStore] No server found for resource URI: ${uri}`);
+
return false;
}
const connection = this.connections.get(serverName);
if (!connection) {
console.error(`[MCPStore] No connection found for server: ${serverName}`);
+
return false;
}
@@ -1333,9 +1487,11 @@ class MCPStore {
try {
await MCPService.subscribeResource(connection, uri);
mcpResourceStore.addSubscription(uri, serverName);
+
return true;
} catch (error) {
console.error(`[MCPStore] Failed to subscribe to resource ${uri}:`, error);
+
return false;
}
}
@@ -1347,21 +1503,25 @@ class MCPStore {
const serverName = mcpResourceStore.findServerForUri(uri);
if (!serverName) {
console.error(`[MCPStore] No server found for resource URI: ${uri}`);
+
return false;
}
const connection = this.connections.get(serverName);
if (!connection) {
console.error(`[MCPStore] No connection found for server: ${serverName}`);
+
return false;
}
try {
await MCPService.unsubscribeResource(connection, uri);
mcpResourceStore.removeSubscription(uri);
+
return true;
} catch (error) {
console.error(`[MCPStore] Failed to unsubscribe from resource ${uri}:`, error);
+
return false;
}
}
@@ -1374,6 +1534,7 @@ class MCPStore {
const resourceInfo = mcpResourceStore.findResourceByUri(uri);
if (!resourceInfo) {
console.error(`[MCPStore] Resource not found: ${uri}`);
+
return null;
}
@@ -1388,6 +1549,7 @@ class MCPStore {
// Fetch content
try {
const content = await this.readResource(uri);
+
if (content) {
mcpResourceStore.updateAttachmentContent(attachment.id, content);
} else {
diff --git a/tools/server/webui/src/lib/utils/mcp.ts b/tools/server/webui/src/lib/utils/mcp.ts
index 41aa4a5b74..1470d114fa 100644
--- a/tools/server/webui/src/lib/utils/mcp.ts
+++ b/tools/server/webui/src/lib/utils/mcp.ts
@@ -1,7 +1,21 @@
-import type { MCPServerSettingsEntry } from '$lib/types';
-import { MCPTransportType, MCPLogLevel, UrlPrefix, MimeTypePrefix } from '$lib/enums';
+import type { MCPServerSettingsEntry, MCPResourceContent, MCPResourceInfo } from '$lib/types';
+import {
+ MCPTransportType,
+ MCPLogLevel,
+ UrlPrefix,
+ MimeTypePrefix,
+ MimeTypeIncludes,
+ UriPattern
+} from '$lib/enums';
import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
-import { Info, AlertTriangle, XCircle } from '@lucide/svelte';
+import {
+ IMAGE_FILE_EXTENSION_REGEX,
+ CODE_FILE_EXTENSION_REGEX,
+ TEXT_FILE_EXTENSION_REGEX,
+ PROTOCOL_PREFIX_REGEX,
+ FILE_EXTENSION_REGEX
+} from '$lib/constants/mcp-resource';
+import { Database, File, FileText, Image, Code, Info, AlertTriangle, XCircle } from '@lucide/svelte';
import type { Component } from 'svelte';
/**