refactor: Cleanup
This commit is contained in:
parent
10c1875cdb
commit
853f711896
|
|
@ -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();
|
||||
});
|
||||
</script>
|
||||
|
||||
<ChatFormFileInputInvisible bind:this={fileInputRef} onFileSelect={handleFileSelect} />
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@
|
|||
extras?: DatabaseMessage['extra']
|
||||
): ToolResultLine[] {
|
||||
const lines = toolResult.split('\n');
|
||||
|
||||
return lines.map((line) => {
|
||||
const match = line.match(ATTACHMENT_SAVED_REGEX);
|
||||
if (!match || !extras) return { text: line };
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@
|
|||
{key}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<span class="max-w-xs break-all">{value}</span>
|
||||
</Tooltip.Content>
|
||||
|
|
@ -161,7 +162,9 @@
|
|||
>
|
||||
<div class="space-y-2">
|
||||
<div class="h-3 w-3/4 animate-pulse rounded bg-foreground/20"></div>
|
||||
|
||||
<div class="h-3 w-full animate-pulse rounded bg-foreground/20"></div>
|
||||
|
||||
<div class="h-3 w-5/6 animate-pulse rounded bg-foreground/20"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -117,9 +117,11 @@
|
|||
onclick={() => (activeView = ChatMessageStatsView.READING)}
|
||||
>
|
||||
<BookOpenText class="h-3 w-3" />
|
||||
|
||||
<span class="sr-only">Reading</span>
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Reading (prompt processing)</p>
|
||||
</Tooltip.Content>
|
||||
|
|
@ -139,9 +141,11 @@
|
|||
disabled={isGenerationDisabled}
|
||||
>
|
||||
<Sparkles class="h-3 w-3" />
|
||||
|
||||
<span class="sr-only">Generation</span>
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>
|
||||
{isGenerationDisabled
|
||||
|
|
@ -150,6 +154,7 @@
|
|||
</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
|
||||
{#if hasAgenticStats}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
|
|
@ -203,12 +208,14 @@
|
|||
value="{predictedTokens?.toLocaleString()} tokens"
|
||||
tooltipLabel="Generated tokens"
|
||||
/>
|
||||
|
||||
<BadgeChatStatistic
|
||||
class="bg-transparent"
|
||||
icon={Clock}
|
||||
value={formattedTime}
|
||||
tooltipLabel="Generation time"
|
||||
/>
|
||||
|
||||
<BadgeChatStatistic
|
||||
class="bg-transparent"
|
||||
icon={Gauge}
|
||||
|
|
@ -222,12 +229,14 @@
|
|||
value="{agenticTimings!.toolCallsCount} calls"
|
||||
tooltipLabel="Tool calls executed"
|
||||
/>
|
||||
|
||||
<BadgeChatStatistic
|
||||
class="bg-transparent"
|
||||
icon={Clock}
|
||||
value={formattedAgenticToolsTime}
|
||||
tooltipLabel="Tool execution time"
|
||||
/>
|
||||
|
||||
<BadgeChatStatistic
|
||||
class="bg-transparent"
|
||||
icon={Gauge}
|
||||
|
|
@ -241,12 +250,14 @@
|
|||
value="{agenticTimings!.turns} turns"
|
||||
tooltipLabel="Agentic turns (LLM calls)"
|
||||
/>
|
||||
|
||||
<BadgeChatStatistic
|
||||
class="bg-transparent"
|
||||
icon={WholeWord}
|
||||
value="{agenticTimings!.llm.predicted_n.toLocaleString()} tokens"
|
||||
tooltipLabel="Total tokens generated"
|
||||
/>
|
||||
|
||||
<BadgeChatStatistic
|
||||
class="bg-transparent"
|
||||
icon={Clock}
|
||||
|
|
@ -260,12 +271,14 @@
|
|||
value="{promptTokens} tokens"
|
||||
tooltipLabel="Prompt tokens"
|
||||
/>
|
||||
|
||||
<BadgeChatStatistic
|
||||
class="bg-transparent"
|
||||
icon={Clock}
|
||||
value={formattedPromptTime ?? '0s'}
|
||||
tooltipLabel="Prompt processing time"
|
||||
/>
|
||||
|
||||
<BadgeChatStatistic
|
||||
class="bg-transparent"
|
||||
icon={Gauge}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
<div class="mt-2 flex justify-end gap-2">
|
||||
<Button class="h-8 px-3" onclick={editCtx.cancel} size="sm" variant="outline">
|
||||
<X class="mr-1 h-3 w-3" />
|
||||
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
|
|
@ -127,6 +128,7 @@
|
|||
size="sm"
|
||||
>
|
||||
<Check class="mr-1 h-3 w-3" />
|
||||
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -174,6 +176,7 @@
|
|||
<div
|
||||
class="pointer-events-none absolute right-0 bottom-0 left-0 h-48 bg-gradient-to-t from-muted to-transparent"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="pointer-events-none absolute right-0 bottom-4 left-0 flex justify-center opacity-0 transition-opacity group-hover/expand:opacity-100"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -28,13 +28,16 @@
|
|||
);
|
||||
await copyToClipboard(clipboardContent, 'Message copied to clipboard');
|
||||
},
|
||||
|
||||
delete: async (message: DatabaseMessage) => {
|
||||
await chatStore.deleteMessage(message.id);
|
||||
refreshAllMessages();
|
||||
},
|
||||
|
||||
navigateToSibling: async (siblingId: string) => {
|
||||
await conversationsStore.navigateToSibling(siblingId);
|
||||
},
|
||||
|
||||
editWithBranching: async (
|
||||
message: DatabaseMessage,
|
||||
newContent: string,
|
||||
|
|
@ -44,6 +47,7 @@
|
|||
await chatStore.editMessageWithBranching(message.id, newContent, newExtras);
|
||||
refreshAllMessages();
|
||||
},
|
||||
|
||||
editWithReplacement: async (
|
||||
message: DatabaseMessage,
|
||||
newContent: string,
|
||||
|
|
@ -53,6 +57,7 @@
|
|||
await chatStore.editAssistantMessage(message.id, newContent, shouldBranch);
|
||||
refreshAllMessages();
|
||||
},
|
||||
|
||||
editUserMessagePreserveResponses: async (
|
||||
message: DatabaseMessage,
|
||||
newContent: string,
|
||||
|
|
@ -62,11 +67,13 @@
|
|||
await chatStore.editUserMessagePreserveResponses(message.id, newContent, newExtras);
|
||||
refreshAllMessages();
|
||||
},
|
||||
|
||||
regenerateWithBranching: async (message: DatabaseMessage, modelOverride?: string) => {
|
||||
onUserAction?.();
|
||||
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
|
||||
refreshAllMessages();
|
||||
},
|
||||
|
||||
continueAssistantMessage: async (message: DatabaseMessage) => {
|
||||
onUserAction?.();
|
||||
await chatStore.continueAssistantMessage(message.id);
|
||||
|
|
|
|||
|
|
@ -311,12 +311,6 @@
|
|||
let activeSection = $state<SettingsSectionTitle>(
|
||||
initialSection ?? SETTINGS_SECTION_TITLES.GENERAL
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (initialSection) {
|
||||
activeSection = initialSection;
|
||||
}
|
||||
});
|
||||
let currentSection = $derived(
|
||||
settingSections.find((section) => section.title === activeSection) || settingSections[0]
|
||||
);
|
||||
|
|
@ -326,6 +320,12 @@
|
|||
let canScrollRight = $state(false);
|
||||
let scrollContainer: HTMLDivElement | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
if (initialSection) {
|
||||
activeSection = initialSection;
|
||||
}
|
||||
});
|
||||
|
||||
function handleThemeChange(newTheme: string) {
|
||||
localConfig.theme = newTheme;
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
const blob = new Blob([extra.content], { type: extra.mimeType || 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
||||
a.href = url;
|
||||
a.download = extra.name || 'resource.txt';
|
||||
document.body.appendChild(a);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Model Information</Dialog.Title>
|
||||
|
||||
<Dialog.Description>Current model details and capabilities</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
|
|
@ -108,6 +109,7 @@
|
|||
{#if serverProps?.default_generation_settings?.n_ctx}
|
||||
<Table.Row>
|
||||
<Table.Cell class="h-10 align-middle font-medium">Context Size</Table.Cell>
|
||||
|
||||
<Table.Cell
|
||||
>{formatNumber(serverProps.default_generation_settings.n_ctx)} tokens</Table.Cell
|
||||
>
|
||||
|
|
@ -117,6 +119,7 @@
|
|||
<Table.Cell class="h-10 align-middle font-medium text-red-500"
|
||||
>Context Size</Table.Cell
|
||||
>
|
||||
|
||||
<Table.Cell class="text-red-500">Not available</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
|
|
@ -125,6 +128,7 @@
|
|||
{#if modelMeta?.n_ctx_train}
|
||||
<Table.Row>
|
||||
<Table.Cell class="h-10 align-middle font-medium">Training Context</Table.Cell>
|
||||
|
||||
<Table.Cell>{formatNumber(modelMeta.n_ctx_train)} tokens</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
|
|
@ -133,6 +137,7 @@
|
|||
{#if modelMeta?.size}
|
||||
<Table.Row>
|
||||
<Table.Cell class="h-10 align-middle font-medium">Model Size</Table.Cell>
|
||||
|
||||
<Table.Cell>{formatFileSize(modelMeta.size)}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
|
|
@ -141,6 +146,7 @@
|
|||
{#if modelMeta?.n_params}
|
||||
<Table.Row>
|
||||
<Table.Cell class="h-10 align-middle font-medium">Parameters</Table.Cell>
|
||||
|
||||
<Table.Cell>{formatParameters(modelMeta.n_params)}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
|
|
@ -149,6 +155,7 @@
|
|||
{#if modelMeta?.n_embd}
|
||||
<Table.Row>
|
||||
<Table.Cell class="align-middle font-medium">Embedding Size</Table.Cell>
|
||||
|
||||
<Table.Cell>{formatNumber(modelMeta.n_embd)}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
|
|
@ -157,6 +164,7 @@
|
|||
{#if modelMeta?.n_vocab}
|
||||
<Table.Row>
|
||||
<Table.Cell class="align-middle font-medium">Vocabulary Size</Table.Cell>
|
||||
|
||||
<Table.Cell>{formatNumber(modelMeta.n_vocab)} tokens</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
|
|
@ -172,6 +180,7 @@
|
|||
<!-- Total Slots -->
|
||||
<Table.Row>
|
||||
<Table.Cell class="align-middle font-medium">Parallel Slots</Table.Cell>
|
||||
|
||||
<Table.Cell>{serverProps.total_slots}</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
|
|
@ -179,6 +188,7 @@
|
|||
{#if modalities.length > 0}
|
||||
<Table.Row>
|
||||
<Table.Cell class="align-middle font-medium">Modalities</Table.Cell>
|
||||
|
||||
<Table.Cell>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<BadgeModality {modalities} />
|
||||
|
|
@ -190,6 +200,7 @@
|
|||
<!-- Build Info -->
|
||||
<Table.Row>
|
||||
<Table.Cell class="align-middle font-medium">Build Info</Table.Cell>
|
||||
|
||||
<Table.Cell class="align-middle font-mono text-xs"
|
||||
>{serverProps.build_info}</Table.Cell
|
||||
>
|
||||
|
|
@ -199,6 +210,7 @@
|
|||
{#if serverProps.chat_template}
|
||||
<Table.Row>
|
||||
<Table.Cell class="align-middle font-medium">Chat Template</Table.Cell>
|
||||
|
||||
<Table.Cell class="py-10">
|
||||
<div class="max-h-120 overflow-y-auto rounded-md bg-muted p-4">
|
||||
<pre
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface ResourceTreeNode {
|
|||
export function parseResourcePath(uri: string): string[] {
|
||||
try {
|
||||
const withoutProtocol = uri.replace(/^[a-z]+:\/\//, '');
|
||||
|
||||
return withoutProtocol.split('/').filter((p) => 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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
[MimeTypeImage.JPEG]: 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
[MimeTypeImage.PNG]: 'png',
|
||||
[MimeTypeImage.GIF]: 'gif',
|
||||
[MimeTypeImage.WEBP]: 'webp'
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Tool[]> {
|
||||
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<Prompt[]> {
|
||||
try {
|
||||
const result = await connection.client.listPrompts();
|
||||
|
||||
return result.prompts ?? [];
|
||||
} catch (error) {
|
||||
console.warn(`[MCPService][${connection.serverName}] Failed to list prompts:`, error);
|
||||
|
|
|
|||
|
|
@ -119,13 +119,16 @@ class AgenticStore {
|
|||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
private updateSession(conversationId: string, update: Partial<AgenticSession>): 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string> | 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<string, MCPServerConfig> = {};
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<string, unknown>) ?? {
|
||||
|
|
@ -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<string, unknown>): Record<string, unknown> {
|
||||
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<string, Record<string, unknown>>;
|
||||
|
|
@ -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<MCPPromptInfo[]> {
|
||||
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<GetPromptResult> {
|
||||
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<ToolExecutionResult> {
|
||||
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<string, unknown>): Record<string, unknown> {
|
||||
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<string, unknown>;
|
||||
} 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<string, string> | 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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue