feat: MCP Resources

This commit is contained in:
Aleksander Grygier 2026-02-07 02:49:49 +01:00
parent c21bd358ce
commit 3287a41af9
18 changed files with 420 additions and 37 deletions

View File

@ -0,0 +1,99 @@
<script lang="ts">
import { FileText, Database, Image, Code, File } from '@lucide/svelte';
import { cn } from '$lib/components/ui/utils';
import { mcpStore } from '$lib/stores/mcp.svelte';
import type { DatabaseMessageExtraMcpResource } from '$lib/types';
import {
IMAGE_FILE_EXTENSION_REGEX,
CODE_FILE_EXTENSION_REGEX,
TEXT_FILE_EXTENSION_REGEX
} from '$lib/constants/mcp-resource';
import { MimeTypePrefix, MimeTypeIncludes, UriPattern } from '$lib/enums';
import * as Tooltip from '$lib/components/ui/tooltip';
import { ActionIconRemove } from '$lib/components/app';
interface Props {
extra: DatabaseMessageExtraMcpResource;
readonly?: boolean;
onRemove?: () => void;
onClick?: (event?: MouseEvent) => void;
class?: string;
}
let { extra, readonly = true, onRemove, onClick, class: className }: Props = $props();
function getResourceIcon(mimeType?: string, uri?: string) {
const mime = mimeType?.toLowerCase() || '';
const u = uri?.toLowerCase() || '';
if (mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u)) {
return Image;
}
if (
mime.includes(MimeTypeIncludes.JSON) ||
mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
CODE_FILE_EXTENSION_REGEX.test(u)
) {
return Code;
}
if (mime.includes(MimeTypePrefix.TEXT) || TEXT_FILE_EXTENSION_REGEX.test(u)) {
return FileText;
}
if (u.includes(UriPattern.DATABASE_KEYWORD) || u.includes(UriPattern.DATABASE_SCHEME)) {
return Database;
}
return File;
}
const ResourceIcon = $derived(getResourceIcon(extra.mimeType, extra.uri));
const serverName = $derived(mcpStore.getServerDisplayName(extra.serverName));
const favicon = $derived(mcpStore.getServerFavicon(extra.serverName));
</script>
<Tooltip.Root>
<Tooltip.Trigger>
<button
type="button"
class={cn(
'flex flex-shrink-0 items-center gap-2 rounded-md border border-border/50 bg-muted/30 p-0.5 px-2 text-sm',
onClick && 'cursor-pointer hover:bg-muted/50',
className
)}
onclick={(e) => {
e.stopPropagation();
onClick?.(e);
}}
disabled={!onClick}
>
<ResourceIcon class="h-3.5 w-3.5 text-muted-foreground" />
<span class="max-w-[150px] truncate">
{extra.name}
</span>
{#if !readonly && onRemove}
<ActionIconRemove class="bg-transparent" id={extra.uri} onRemove={() => onRemove?.()} />
{/if}
</button>
</Tooltip.Trigger>
<Tooltip.Content>
<div class="flex items-center gap-1 text-xs">
{#if favicon}
<img
src={favicon}
alt=""
class="h-3 w-3 shrink-0 rounded-sm"
onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none';
}}
/>
{/if}
<span class="truncate">
{serverName}
</span>
</div>
</Tooltip.Content>
</Tooltip.Root>

View File

@ -1,15 +1,17 @@
<script lang="ts">
import {
ChatAttachmentMcpPrompt,
ChatAttachmentMcpResourceStored,
ChatAttachmentThumbnailImage,
ChatAttachmentThumbnailFile,
HorizontalScrollCarousel,
DialogChatAttachmentPreview,
DialogChatAttachmentsViewAll
DialogChatAttachmentsViewAll,
DialogMcpResourcePreview
} from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import { AttachmentType } from '$lib/enums';
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
import type { DatabaseMessageExtraMcpPrompt, DatabaseMessageExtraMcpResource } from '$lib/types';
import { getAttachmentDisplayItems } from '$lib/utils';
interface Props {
@ -52,6 +54,8 @@
let isScrollable = $state(false);
let previewDialogOpen = $state(false);
let previewItem = $state<ChatAttachmentPreviewItem | null>(null);
let mcpResourcePreviewOpen = $state(false);
let mcpResourcePreviewExtra = $state<DatabaseMessageExtraMcpResource | null>(null);
let showViewAll = $derived(limitToSingleRow && displayItems.length > 0 && isScrollable);
let viewAllDialogOpen = $state(false);
@ -70,6 +74,13 @@
previewDialogOpen = true;
}
function openMcpResourcePreview(extra: DatabaseMessageExtraMcpResource, event?: MouseEvent) {
event?.stopPropagation();
event?.preventDefault();
mcpResourcePreviewExtra = extra;
mcpResourcePreviewOpen = true;
}
$effect(() => {
if (carouselRef && displayItems.length) {
carouselRef.resetScroll();
@ -111,6 +122,15 @@
onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined}
/>
{/if}
{:else if item.isMcpResource && item.attachment?.type === AttachmentType.MCP_RESOURCE}
<ChatAttachmentMcpResourceStored
class="flex-shrink-0 {limitToSingleRow ? 'first:ml-4 last:mr-4' : ''}"
extra={item.attachment as DatabaseMessageExtraMcpResource}
{readonly}
onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined}
onClick={(event) =>
openMcpResourcePreview(item.attachment as DatabaseMessageExtraMcpResource, event)}
/>
{:else if item.isImage && item.preview}
<ChatAttachmentThumbnailImage
class="flex-shrink-0 cursor-pointer {limitToSingleRow ? 'first:ml-4 last:mr-4' : ''}"
@ -182,6 +202,14 @@
onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined}
/>
{/if}
{:else if item.isMcpResource && item.attachment?.type === AttachmentType.MCP_RESOURCE}
<ChatAttachmentMcpResourceStored
extra={item.attachment as DatabaseMessageExtraMcpResource}
{readonly}
onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined}
onClick={(event) =>
openMcpResourcePreview(item.attachment as DatabaseMessageExtraMcpResource, event)}
/>
{:else if item.isImage && item.preview}
<ChatAttachmentThumbnailImage
class="cursor-pointer"
@ -238,3 +266,7 @@
{imageClass}
{activeModelId}
/>
{#if mcpResourcePreviewExtra}
<DialogMcpResourcePreview bind:open={mcpResourcePreviewOpen} extra={mcpResourcePreviewExtra} />
{/if}

View File

@ -545,6 +545,9 @@
<DialogMcpResources
bind:open={isResourcePickerOpen}
preSelectedUri={preSelectedResourceUri}
onAttach={(resource) => {
mcpStore.attachResource(resource.uri);
}}
onOpenChange={(newOpen: boolean) => {
if (!newOpen) {
preSelectedResourceUri = undefined;

View File

@ -65,6 +65,12 @@ export { default as ChatAttachmentMcpPrompt } from './ChatAttachments/ChatAttach
*/
export { default as ChatAttachmentMcpResource } from './ChatAttachments/ChatAttachmentMcpResource.svelte';
/**
* Displays a stored MCP Resource attachment (from database extras) with icon,
* name, and server info tooltip. Compact chip style matching live resource display.
*/
export { default as ChatAttachmentMcpResourceStored } from './ChatAttachments/ChatAttachmentMcpResourceStored.svelte';
/**
* Full-size attachment preview component for dialog display. Handles different file types:
* images (full-size display), text files (syntax highlighted), PDFs (text extraction or image preview),

View File

@ -0,0 +1,135 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Download } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { SyntaxHighlightedCode, ActionIconCopyToClipboard } from '$lib/components/app';
import { getLanguageFromFilename } from '$lib/utils';
import { MimeTypePrefix, MimeTypeIncludes } from '$lib/enums';
import type { DatabaseMessageExtraMcpResource } from '$lib/types';
interface Props {
open: boolean;
onOpenChange?: (open: boolean) => void;
extra: DatabaseMessageExtraMcpResource;
}
let { open = $bindable(), onOpenChange, extra }: Props = $props();
const serverName = $derived(mcpStore.getServerDisplayName(extra.serverName));
const favicon = $derived(mcpStore.getServerFavicon(extra.serverName));
function isCode(mimeType?: string, uri?: string): boolean {
const mime = mimeType?.toLowerCase() || '';
const u = uri?.toLowerCase() || '';
return (
mime.includes(MimeTypeIncludes.JSON) ||
mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
/\.(js|ts|json|yaml|yml|xml|html|css|py|rs|go|java|cpp|c|h|rb|sh|toml)$/i.test(u)
);
}
function isImage(mimeType?: string, uri?: string): boolean {
const mime = mimeType?.toLowerCase() || '';
const u = uri?.toLowerCase() || '';
return mime.startsWith(MimeTypePrefix.IMAGE) || /\.(png|jpg|jpeg|gif|svg|webp)$/i.test(u);
}
function getLanguage(): string {
if (extra.mimeType?.includes(MimeTypeIncludes.JSON)) return 'json';
if (extra.mimeType?.includes(MimeTypeIncludes.JAVASCRIPT)) return 'javascript';
if (extra.mimeType?.includes(MimeTypeIncludes.TYPESCRIPT)) return 'typescript';
// Try to detect from URI/name
const name = extra.name || extra.uri || '';
return getLanguageFromFilename(name) || 'plaintext';
}
function handleDownload() {
if (!extra.content) return;
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);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
</script>
<Dialog.Root bind:open {onOpenChange}>
<Dialog.Content class="grid max-h-[90vh] max-w-5xl overflow-hidden sm:w-auto sm:max-w-6xl">
<Dialog.Header>
<Dialog.Title class="pr-8">{extra.name}</Dialog.Title>
<Dialog.Description>
<div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground">{extra.uri}</span>
{#if serverName}
<span class="flex items-center gap-1 text-xs text-muted-foreground">
·
{#if favicon}
<img
src={favicon}
alt=""
class="h-3 w-3 shrink-0 rounded-sm"
onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none';
}}
/>
{/if}
{serverName}
</span>
{/if}
{#if extra.mimeType}
<span class="rounded bg-muted px-1.5 py-0.5 text-xs">{extra.mimeType}</span>
{/if}
</div>
</Dialog.Description>
</Dialog.Header>
<div class="flex justify-end gap-1">
<ActionIconCopyToClipboard
text={extra.content}
canCopy={!!extra.content}
ariaLabel="Copy content"
/>
<Button
variant="ghost"
size="sm"
class="h-7 w-7 p-0"
onclick={handleDownload}
disabled={!extra.content}
title="Download content"
>
<Download class="h-3.5 w-3.5" />
</Button>
</div>
<div class="overflow-auto">
{#if isImage(extra.mimeType, extra.uri) && extra.content}
<div class="flex items-center justify-center">
<img
src={extra.content.startsWith('data:')
? extra.content
: `data:${extra.mimeType || 'image/png'};base64,${extra.content}`}
alt={extra.name}
class="max-h-[70vh] max-w-full rounded object-contain"
/>
</div>
{:else if isCode(extra.mimeType, extra.uri) && extra.content}
<SyntaxHighlightedCode code={extra.content} language={getLanguage()} maxHeight="70vh" />
{:else if extra.content}
<pre
class="max-h-[70vh] overflow-auto rounded-md border bg-muted/30 p-4 font-mono text-sm break-words whitespace-pre-wrap">{extra.content}</pre>
{:else}
<div class="py-8 text-center text-sm text-muted-foreground">No content available</div>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>

View File

@ -473,3 +473,27 @@ export { default as DialogModelInformation } from './DialogModelInformation.svel
* ```
*/
export { default as DialogMcpResources } from './DialogMcpResources.svelte';
/**
* **DialogMcpResourcePreview** - MCP resource content preview
*
* Dialog for previewing the content of a stored MCP resource attachment.
* Displays the resource content with syntax highlighting for code,
* image rendering for images, and plain text for other content.
*
* **Features:**
* - Syntax highlighted code preview
* - Image rendering for image resources
* - Copy to clipboard and download actions
* - Server name and favicon display
* - MIME type badge
*
* @example
* ```svelte
* <DialogMcpResourcePreview
* bind:open={previewOpen}
* extra={mcpResourceExtra}
* />
* ```
*/
export { default as DialogMcpResourcePreview } from './DialogMcpResourcePreview.svelte';

View File

@ -1,3 +1,4 @@
export const ATTACHMENT_LABEL_FILE = 'File';
export const ATTACHMENT_LABEL_PDF_FILE = 'PDF File';
export const ATTACHMENT_LABEL_MCP_PROMPT = 'MCP Prompt';
export const ATTACHMENT_LABEL_MCP_RESOURCE = 'MCP Resource';

View File

@ -5,6 +5,7 @@ export enum AttachmentType {
AUDIO = 'AUDIO',
IMAGE = 'IMAGE',
MCP_PROMPT = 'MCP_PROMPT',
MCP_RESOURCE = 'MCP_RESOURCE',
PDF = 'PDF',
TEXT = 'TEXT',
LEGACY_CONTEXT = 'context' // Legacy attachment type for backward compatibility

View File

@ -2,7 +2,8 @@ import { getJsonHeaders, formatAttachmentText, isAbortError } from '$lib/utils';
import { AGENTIC_REGEX } from '$lib/constants/agentic';
import {
ATTACHMENT_LABEL_PDF_FILE,
ATTACHMENT_LABEL_MCP_PROMPT
ATTACHMENT_LABEL_MCP_PROMPT,
ATTACHMENT_LABEL_MCP_RESOURCE
} from '$lib/constants/attachment-labels';
import {
AttachmentType,
@ -12,7 +13,7 @@ import {
UrlPrefix
} from '$lib/enums';
import type { ApiChatMessageContentPart, ApiChatCompletionToolCall } from '$lib/types/api';
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
import type { DatabaseMessageExtraMcpPrompt, DatabaseMessageExtraMcpResource } from '$lib/types';
import { modelsStore } from '$lib/stores/models.svelte';
/**
@ -796,6 +797,23 @@ export class ChatService {
});
}
const mcpResources = message.extra.filter(
(extra: DatabaseMessageExtra): extra is DatabaseMessageExtraMcpResource =>
extra.type === AttachmentType.MCP_RESOURCE
);
for (const mcpResource of mcpResources) {
contentParts.push({
type: ContentPartType.TEXT,
text: formatAttachmentText(
ATTACHMENT_LABEL_MCP_RESOURCE,
mcpResource.name,
mcpResource.content,
mcpResource.serverName
)
});
}
const result: ApiChatMessageData = {
role: message.role as MessageRole,
content: contentParts

View File

@ -447,6 +447,11 @@ class ChatStore {
if (!content.trim() && (!extras || extras.length === 0)) return;
const activeConv = conversationsStore.activeConversation;
if (activeConv && this.isChatLoadingInternal(activeConv.id)) return;
// Consume MCP resource attachments - converts them to extras and clears the live store
const resourceExtras = mcpStore.consumeResourceAttachmentsAsExtras();
const allExtras = resourceExtras.length > 0 ? [...(extras || []), ...resourceExtras] : extras;
let isNewConversation = false;
if (!activeConv) {
await conversationsStore.createConversation();
@ -478,7 +483,7 @@ class ChatStore {
content,
MessageType.TEXT,
parentIdForUserMessage ?? '-1',
extras
allExtras
);
if (isNewConversation && content)
await conversationsStore.updateConversationName(currentConv.id, content.trim());
@ -686,6 +691,7 @@ class ChatStore {
}
};
const perChatOverrides = conversationsStore.activeConversation?.mcpServerOverrides;
const agenticConfig = agenticStore.getConfig(config(), perChatOverrides);
if (agenticConfig.enabled) {
const agenticResult = await agenticStore.runAgenticFlow({
@ -698,29 +704,16 @@ class ChatStore {
});
if (agenticResult.handled) return;
}
const resourceContext = mcpStore.getResourceContextForChat();
let messagesWithResources = allMessages;
if (resourceContext) {
messagesWithResources = allMessages.map((msg, idx) => {
if (idx === allMessages.length - 1 && msg.role === MessageRole.USER) {
return {
...msg,
content: resourceContext + '\n\n' + msg.content
};
}
return msg;
});
mcpStore.clearResourceAttachments();
}
const completionOptions = {
...this.getApiOptions(),
...(effectiveModel ? { model: effectiveModel } : {}),
...streamCallbacks
};
await ChatService.sendMessage(
messagesWithResources,
{
...this.getApiOptions(),
...(effectiveModel ? { model: effectiveModel } : {}),
...streamCallbacks
},
allMessages,
completionOptions,
assistantMessage.convId,
abortController.signal
);

View File

@ -11,6 +11,7 @@
*/
import { SvelteMap } from 'svelte/reactivity';
import { AttachmentType } from '$lib/enums';
import type {
MCPResource,
MCPResourceTemplate,
@ -524,6 +525,43 @@ class MCPResourceStore {
return parts.join('');
}
/**
* Convert current resource attachments to DatabaseMessageExtra[] for persisting with a message.
* Each attachment becomes a DatabaseMessageExtraMcpResource stored on the user message.
*/
toMessageExtras(): import('$lib/types').DatabaseMessageExtraMcpResource[] {
const extras: import('$lib/types').DatabaseMessageExtraMcpResource[] = [];
for (const attachment of this._attachments) {
if (attachment.error) continue;
if (!attachment.content || attachment.content.length === 0) continue;
const resourceName = attachment.resource.title || attachment.resource.name;
const contentParts: string[] = [];
for (const content of attachment.content) {
if ('text' in content && content.text) {
contentParts.push(content.text);
} else if ('blob' in content && content.blob) {
contentParts.push(`[Binary content: ${content.mimeType || 'unknown type'}]`);
}
}
if (contentParts.length > 0) {
extras.push({
type: AttachmentType.MCP_RESOURCE,
name: resourceName,
uri: attachment.resource.uri,
serverName: attachment.resource.serverName,
content: contentParts.join('\n'),
mimeType: attachment.resource.mimeType
});
}
}
return extras;
}
}
export const mcpResourceStore = new MCPResourceStore();

View File

@ -23,7 +23,7 @@ import { browser } from '$app/environment';
import { MCPService } from '$lib/services/mcp.service';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
import { parseMcpServerSettings, detectMcpTransportFromUrl } from '$lib/utils';
import { parseMcpServerSettings, detectMcpTransportFromUrl, getFaviconUrl } from '$lib/utils';
import { MCPConnectionPhase, MCPLogLevel, HealthCheckStatus, MCPRefType } from '$lib/enums';
import {
DEFAULT_MCP_CONFIG,
@ -298,17 +298,13 @@ class MCPStore {
/**
* Get favicon URL for an MCP server by its ID.
* Uses Google's favicon service for consistent display.
* Returns null if server is not found.
*/
getServerFavicon(serverId: string): string | null {
const server = this.getServerById(serverId);
if (!server) return null;
try {
const url = new URL(server.url);
return `${url.origin}/favicon.ico`;
} catch {
return null;
}
return getFaviconUrl(server.url);
}
isAnyServerLoading(): boolean {
@ -1425,6 +1421,18 @@ class MCPStore {
getResourceContextForChat(): string {
return mcpResourceStore.formatAttachmentsForContext();
}
/**
* Convert current resource attachments to DatabaseMessageExtra[] and clear them.
* Called during message send to persist resources with the user message.
*/
consumeResourceAttachmentsAsExtras(): import('$lib/types').DatabaseMessageExtraMcpResource[] {
const extras = mcpResourceStore.toMessageExtras();
if (extras.length > 0) {
mcpResourceStore.clearAttachments();
}
return extras;
}
}
export const mcpStore = new MCPStore();

View File

@ -25,6 +25,7 @@ export interface ChatAttachmentDisplayItem {
preview?: string;
isImage: boolean;
isMcpPrompt?: boolean;
isMcpResource?: boolean;
isLoading?: boolean;
loadError?: string;
uploadedFile?: ChatUploadedFile;

View File

@ -61,12 +61,22 @@ export interface DatabaseMessageExtraMcpPrompt {
arguments?: Record<string, string>;
}
export interface DatabaseMessageExtraMcpResource {
type: AttachmentType.MCP_RESOURCE;
name: string;
uri: string;
serverName: string;
content: string;
mimeType?: string;
}
export type DatabaseMessageExtra =
| DatabaseMessageExtraImageFile
| DatabaseMessageExtraTextFile
| DatabaseMessageExtraAudioFile
| DatabaseMessageExtraPdfFile
| DatabaseMessageExtraMcpPrompt
| DatabaseMessageExtraMcpResource
| DatabaseMessageExtraLegacyContext;
export interface DatabaseMessage {

View File

@ -59,6 +59,7 @@ export type {
DatabaseMessageExtraImageFile,
DatabaseMessageExtraLegacyContext,
DatabaseMessageExtraMcpPrompt,
DatabaseMessageExtraMcpResource,
DatabaseMessageExtraPdfFile,
DatabaseMessageExtraTextFile,
DatabaseMessageExtra,

View File

@ -20,6 +20,13 @@ function isMcpPromptAttachment(attachment: DatabaseMessageExtra): boolean {
return attachment.type === AttachmentType.MCP_PROMPT;
}
/**
* Check if an attachment is an MCP resource
*/
function isMcpResourceAttachment(attachment: DatabaseMessageExtra): boolean {
return attachment.type === AttachmentType.MCP_RESOURCE;
}
/**
* Gets the file type category from an uploaded file, checking both MIME type and extension
*/
@ -63,6 +70,7 @@ export function getAttachmentDisplayItems(
for (const [index, attachment] of attachments.entries()) {
const isImage = isImageFile(attachment);
const isMcpPrompt = isMcpPromptAttachment(attachment);
const isMcpResource = isMcpResourceAttachment(attachment);
items.push({
id: `attachment-${index}`,
@ -70,6 +78,7 @@ export function getAttachmentDisplayItems(
preview: isImage && 'base64Url' in attachment ? attachment.base64Url : undefined,
isImage,
isMcpPrompt,
isMcpResource,
attachment,
attachmentIndex: index,
textContent: 'content' in attachment ? attachment.content : undefined

View File

@ -5,6 +5,7 @@ import type {
DatabaseMessageExtraTextFile,
DatabaseMessageExtraLegacyContext,
DatabaseMessageExtraMcpPrompt,
DatabaseMessageExtraMcpResource,
ClipboardTextAttachment,
ClipboardMcpPromptAttachment,
ClipboardAttachment,
@ -104,7 +105,7 @@ export function formatMessageForClipboard(
extras?: DatabaseMessageExtra[],
asPlainText: boolean = false
): string {
// Filter text-like attachments (TEXT, LEGACY_CONTEXT, and MCP_PROMPT types)
// Filter text-like attachments (TEXT, LEGACY_CONTEXT, MCP_PROMPT, and MCP_RESOURCE types)
const textAttachments =
extras?.filter(
(
@ -112,10 +113,12 @@ export function formatMessageForClipboard(
): extra is
| DatabaseMessageExtraTextFile
| DatabaseMessageExtraLegacyContext
| DatabaseMessageExtraMcpPrompt =>
| DatabaseMessageExtraMcpPrompt
| DatabaseMessageExtraMcpResource =>
extra.type === AttachmentType.TEXT ||
extra.type === AttachmentType.LEGACY_CONTEXT ||
extra.type === AttachmentType.MCP_PROMPT
extra.type === AttachmentType.MCP_PROMPT ||
extra.type === AttachmentType.MCP_RESOURCE
) ?? [];
if (textAttachments.length === 0) {

View File

@ -1,6 +1,6 @@
import type { MCPServerSettingsEntry } from '$lib/types';
import { MCPTransportType, MCPLogLevel, UrlPrefix, MimeTypePrefix } from '$lib/enums';
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
import { Info, AlertTriangle, XCircle } from '@lucide/svelte';
import type { Component } from 'svelte';
@ -51,12 +51,13 @@ export function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEn
const id =
typeof (entry as { id?: unknown })?.id === 'string' && (entry as { id?: string }).id?.trim()
? (entry as { id: string }).id.trim()
: `server-${index + 1}`;
: `${MCP_SERVER_ID_PREFIX}${index + 1}`;
return {
id,
enabled: Boolean((entry as { enabled?: unknown })?.enabled),
url,
name: (entry as { name?: string })?.name,
requestTimeoutSeconds: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
headers: headers || undefined,
useProxy: Boolean((entry as { useProxy?: unknown })?.useProxy)