feat: MCP Server Details
This commit is contained in:
parent
120f3c978c
commit
f89bcb90ca
|
|
@ -0,0 +1,78 @@
|
|||
<script lang="ts">
|
||||
import { Wrench, Database, MessageSquare, FileText, Sparkles, ListChecks } from '@lucide/svelte';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import type { MCPCapabilitiesInfo } from '$lib/types';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
|
||||
interface Props {
|
||||
capabilities?: MCPCapabilitiesInfo;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { capabilities, class: className }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if capabilities}
|
||||
<div class={cn('space-y-2 text-xs', className)}>
|
||||
<div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#if capabilities.server.tools}
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="h-5 gap-1 bg-green-50 px-1.5 text-[10px] dark:bg-green-950"
|
||||
>
|
||||
<Wrench class="h-3 w-3 text-green-600 dark:text-green-400" />
|
||||
|
||||
Tools
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
{#if capabilities.server.resources}
|
||||
<Badge variant="outline" class="h-5 gap-1 bg-blue-50 px-1.5 text-[10px] dark:bg-blue-950">
|
||||
<Database class="h-3 w-3 text-blue-600 dark:text-blue-400" />
|
||||
|
||||
Resources
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
{#if capabilities.server.prompts}
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="h-5 gap-1 bg-purple-50 px-1.5 text-[10px] dark:bg-purple-950"
|
||||
>
|
||||
<MessageSquare class="h-3 w-3 text-purple-600 dark:text-purple-400" />
|
||||
|
||||
Prompts
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
{#if capabilities.server.logging}
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="h-5 gap-1 bg-orange-50 px-1.5 text-[10px] dark:bg-orange-950"
|
||||
>
|
||||
<FileText class="h-3 w-3 text-orange-600 dark:text-orange-400" />
|
||||
|
||||
Logging
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
{#if capabilities.server.completions}
|
||||
<Badge variant="outline" class="h-5 gap-1 bg-cyan-50 px-1.5 text-[10px] dark:bg-cyan-950">
|
||||
<Sparkles class="h-3 w-3 text-cyan-600 dark:text-cyan-400" />
|
||||
|
||||
Completions
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
{#if capabilities.server.tasks}
|
||||
<Badge variant="outline" class="h-5 gap-1 bg-pink-50 px-1.5 text-[10px] dark:bg-pink-950">
|
||||
<ListChecks class="h-3 w-3 text-pink-600 dark:text-pink-400" />
|
||||
|
||||
Tasks
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
import { ChevronDown, ChevronRight } from '@lucide/svelte';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import type { MCPConnectionLog } from '$lib/types';
|
||||
import { formatTime } from '$lib/utils/formatters';
|
||||
import { getMcpLogLevelIcon, getMcpLogLevelClass } from '$lib/utils/mcp';
|
||||
|
||||
interface Props {
|
||||
logs: MCPConnectionLog[];
|
||||
connectionTimeMs?: number;
|
||||
defaultExpanded?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { logs, connectionTimeMs, defaultExpanded = false, class: className }: Props = $props();
|
||||
|
||||
let isExpanded = $state(defaultExpanded);
|
||||
</script>
|
||||
|
||||
{#if logs.length > 0}
|
||||
<Collapsible.Root bind:open={isExpanded} class={className}>
|
||||
<div class="space-y-2">
|
||||
<Collapsible.Trigger
|
||||
class="flex w-full items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{#if isExpanded}
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
<span>Connection Log ({logs.length})</span>
|
||||
{#if connectionTimeMs !== undefined}
|
||||
<span class="ml-1">· Connected in {connectionTimeMs}ms</span>
|
||||
{/if}
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
|
||||
<Collapsible.Content class="mt-2">
|
||||
<div
|
||||
class="max-h-64 space-y-0.5 overflow-y-auto rounded bg-muted/50 p-2 font-mono text-[10px]"
|
||||
>
|
||||
{#each logs as log (log.timestamp.getTime() + log.message)}
|
||||
{@const Icon = getMcpLogLevelIcon(log.level)}
|
||||
<div class={cn('flex items-start gap-1.5', getMcpLogLevelClass(log.level))}>
|
||||
<span class="shrink-0 text-muted-foreground">
|
||||
{formatTime(log.timestamp)}
|
||||
</span>
|
||||
<Icon class="mt-0.5 h-3 w-3 shrink-0" />
|
||||
<span class="break-all">{log.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
{/if}
|
||||
|
|
@ -10,6 +10,9 @@
|
|||
import McpServerCardToolsList from './McpServerCardToolsList.svelte';
|
||||
import McpServerCardEditForm from './McpServerCardEditForm.svelte';
|
||||
import McpServerCardDeleteDialog from './McpServerCardDeleteDialog.svelte';
|
||||
import McpServerInfo from './McpServerInfo.svelte';
|
||||
import McpConnectionLogs from './McpConnectionLogs.svelte';
|
||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||
|
||||
interface Props {
|
||||
server: MCPServerSettingsEntry;
|
||||
|
|
@ -38,6 +41,7 @@
|
|||
? healthState.logs
|
||||
: []
|
||||
);
|
||||
|
||||
let serverInfo = $derived(
|
||||
healthState.status === HealthCheckStatus.Success ? healthState.serverInfo : undefined
|
||||
);
|
||||
|
|
@ -61,6 +65,12 @@
|
|||
let showDeleteDialog = $state(false);
|
||||
let editFormRef: McpServerCardEditForm | null = $state(null);
|
||||
|
||||
const transportLabels: Record<string, string> = {
|
||||
websocket: 'WebSocket',
|
||||
streamable_http: 'HTTP',
|
||||
sse: 'SSE'
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (!mcpStore.hasHealthCheck(server.id) && server.enabled && server.url.trim()) {
|
||||
mcpClient.runHealthCheck(server);
|
||||
|
|
@ -101,7 +111,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="!gap-4 bg-muted/30 p-4">
|
||||
<Card.Root class="!gap-3 bg-muted/30 p-4">
|
||||
{#if isEditing}
|
||||
<McpServerCardEditForm
|
||||
bind:this={editFormRef}
|
||||
|
|
@ -114,38 +124,60 @@
|
|||
<McpServerCardHeader
|
||||
{displayName}
|
||||
{faviconUrl}
|
||||
serverUrl={server.url}
|
||||
enabled={server.enabled}
|
||||
{isHealthChecking}
|
||||
{isConnected}
|
||||
{isError}
|
||||
{onToggle}
|
||||
{serverInfo}
|
||||
{capabilities}
|
||||
/>
|
||||
|
||||
{#if isError && errorMessage}
|
||||
<p class="mt-3 text-xs text-destructive">{errorMessage}</p>
|
||||
<p class="mt-2 text-xs text-destructive">{errorMessage}</p>
|
||||
{/if}
|
||||
|
||||
{#if tools.length === 0 && server.url.trim()}
|
||||
<div class="mt-3 flex items-center justify-end gap-1">
|
||||
<McpServerCardActions
|
||||
{isHealthChecking}
|
||||
onEdit={startEditing}
|
||||
onRefresh={handleHealthCheck}
|
||||
onDelete={handleDeleteClick}
|
||||
/>
|
||||
</div>
|
||||
{#if isConnected && serverInfo?.description}
|
||||
<p class="mt-3 line-clamp-2 text-xs text-muted-foreground">
|
||||
{serverInfo.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if tools.length > 0}
|
||||
<McpServerCardToolsList
|
||||
{tools}
|
||||
<div class="mt-2 grid gap-3">
|
||||
{#if isConnected && instructions}
|
||||
<McpServerInfo {instructions} class="mt-3" />
|
||||
{/if}
|
||||
|
||||
{#if tools.length > 0}
|
||||
<McpServerCardToolsList {tools} />
|
||||
{/if}
|
||||
|
||||
{#if connectionLogs.length > 0}
|
||||
<McpConnectionLogs logs={connectionLogs} {connectionTimeMs} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-between gap-4">
|
||||
{#if transportType || protocolVersion}
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
{#if transportType}
|
||||
<Badge variant="outline" class="h-5 gap-1 px-1.5 text-[10px]">
|
||||
{transportLabels[transportType] || transportType}
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
{#if protocolVersion}
|
||||
<Badge variant="outline" class="h-5 gap-1 px-1.5 text-[10px]">
|
||||
MCP {protocolVersion}
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<McpServerCardActions
|
||||
{isHealthChecking}
|
||||
onEdit={startEditing}
|
||||
onRefresh={handleHealthCheck}
|
||||
onDelete={handleDeleteClick}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,71 +1,71 @@
|
|||
<script lang="ts">
|
||||
import { Cable, ExternalLink } from '@lucide/svelte';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import type { MCPServerInfo, MCPCapabilitiesInfo } from '$lib/types';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import McpCapabilitiesBadges from './McpCapabilitiesBadges.svelte';
|
||||
|
||||
interface Props {
|
||||
displayName: string;
|
||||
faviconUrl: string | null;
|
||||
serverUrl: string;
|
||||
enabled: boolean;
|
||||
isHealthChecking: boolean;
|
||||
isConnected: boolean;
|
||||
isError: boolean;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
serverInfo?: MCPServerInfo;
|
||||
capabilities?: MCPCapabilitiesInfo;
|
||||
}
|
||||
|
||||
let {
|
||||
displayName,
|
||||
faviconUrl,
|
||||
serverUrl,
|
||||
enabled,
|
||||
isHealthChecking,
|
||||
isConnected,
|
||||
isError,
|
||||
onToggle
|
||||
}: Props = $props();
|
||||
let { displayName, faviconUrl, enabled, onToggle, serverInfo, capabilities }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
{#if faviconUrl}
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
class="h-5 w-5 shrink-0 rounded"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Cable class="h-5 w-5 shrink-0 text-muted-foreground" />
|
||||
{/if}
|
||||
<p class="truncate font-medium">{displayName}</p>
|
||||
{#if serverUrl}
|
||||
<a
|
||||
href={serverUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="shrink-0 text-muted-foreground hover:text-foreground"
|
||||
aria-label="Open server URL"
|
||||
>
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
</a>
|
||||
{/if}
|
||||
{#if isHealthChecking}
|
||||
<span class="shrink-0 text-xs text-muted-foreground">Checking...</span>
|
||||
{:else if isConnected}
|
||||
<span
|
||||
class="shrink-0 rounded bg-green-500/15 px-1.5 py-0.5 text-xs text-green-600 dark:text-green-500"
|
||||
>Connected</span
|
||||
>
|
||||
{:else if isError}
|
||||
<span class="shrink-0 rounded bg-destructive/15 px-1.5 py-0.5 text-xs text-destructive"
|
||||
>Error</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="grid min-w-0 gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if faviconUrl}
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
class="h-5 w-5 shrink-0 rounded"
|
||||
onerror={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted">
|
||||
<Cable class="h-3 w-3 text-muted-foreground" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex shrink-0 items-center">
|
||||
<Switch checked={enabled} onCheckedChange={onToggle} />
|
||||
<p class="truncate leading-none font-medium">
|
||||
{serverInfo?.title || serverInfo?.name || displayName}
|
||||
</p>
|
||||
|
||||
{#if serverInfo?.version}
|
||||
<Badge variant="secondary" class="h-4 shrink-0 px-1 text-[10px]">
|
||||
v{serverInfo.version}
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
{#if serverInfo?.websiteUrl}
|
||||
<a
|
||||
href={serverInfo.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="shrink-0 text-muted-foreground hover:text-foreground"
|
||||
aria-label="Open website"
|
||||
>
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if capabilities}
|
||||
<McpCapabilitiesBadges {capabilities} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center pl-2">
|
||||
<Switch checked={enabled} onCheckedChange={onToggle} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { ChevronDown, ChevronRight } from '@lucide/svelte';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import McpServerCardActions from './McpServerCardActions.svelte';
|
||||
|
||||
interface Tool {
|
||||
name: string;
|
||||
|
|
@ -11,37 +10,33 @@
|
|||
|
||||
interface Props {
|
||||
tools: Tool[];
|
||||
isHealthChecking: boolean;
|
||||
onEdit: () => void;
|
||||
onRefresh: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
let { tools, isHealthChecking, onEdit, onRefresh, onDelete }: Props = $props();
|
||||
let { tools }: Props = $props();
|
||||
|
||||
let isExpanded = $state(false);
|
||||
let toolsCount = $derived(tools.length);
|
||||
</script>
|
||||
|
||||
<Collapsible.Root bind:open={isExpanded}>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<Collapsible.Trigger
|
||||
class="flex flex-1 items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{#if isExpanded}
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
<span>{toolsCount} tools available · Show details</span>
|
||||
</Collapsible.Trigger>
|
||||
<McpServerCardActions {isHealthChecking} {onEdit} {onRefresh} {onDelete} />
|
||||
</div>
|
||||
<Collapsible.Trigger
|
||||
class="flex w-full items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{#if isExpanded}
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
|
||||
<span>{toolsCount} tools available · Show details</span>
|
||||
</Collapsible.Trigger>
|
||||
|
||||
<Collapsible.Content class="mt-2">
|
||||
<div class="max-h-64 space-y-3 overflow-y-auto">
|
||||
{#each tools as tool (tool.name)}
|
||||
<div>
|
||||
<Badge variant="secondary">{tool.name}</Badge>
|
||||
|
||||
{#if tool.description}
|
||||
<p class="mt-1 text-xs text-muted-foreground">{tool.description}</p>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { ChevronDown, ChevronRight } from '@lucide/svelte';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
|
||||
interface Props {
|
||||
instructions?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { instructions, class: className }: Props = $props();
|
||||
|
||||
let isExpanded = $state(false);
|
||||
</script>
|
||||
|
||||
{#if instructions}
|
||||
<Collapsible.Root bind:open={isExpanded} class={className}>
|
||||
<Collapsible.Trigger
|
||||
class="flex w-full items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{#if isExpanded}
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
{:else}
|
||||
<ChevronRight class="h-3.5 w-3.5" />
|
||||
{/if}
|
||||
|
||||
<span>Server instructions</span>
|
||||
</Collapsible.Trigger>
|
||||
|
||||
<Collapsible.Content class="mt-2">
|
||||
<p class="rounded bg-muted/50 p-2 text-xs text-muted-foreground">
|
||||
{instructions}
|
||||
</p>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
{/if}
|
||||
|
|
@ -4,3 +4,6 @@ export { default as McpServerCardActions } from './McpServerCardActions.svelte';
|
|||
export { default as McpServerCardToolsList } from './McpServerCardToolsList.svelte';
|
||||
export { default as McpServerCardEditForm } from './McpServerCardEditForm.svelte';
|
||||
export { default as McpServerCardDeleteDialog } from './McpServerCardDeleteDialog.svelte';
|
||||
export { default as McpServerInfo } from './McpServerInfo.svelte';
|
||||
export { default as McpCapabilitiesBadges } from './McpCapabilitiesBadges.svelte';
|
||||
export { default as McpConnectionLogs } from './McpConnectionLogs.svelte';
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Add New Server Form -->
|
||||
{#if isAddingServer}
|
||||
<Card.Root class="bg-muted/30 p-4">
|
||||
<div class="space-y-4">
|
||||
|
|
@ -113,7 +112,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Server Cards -->
|
||||
{#if servers.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each servers as server (server.id)}
|
||||
|
|
|
|||
|
|
@ -67,3 +67,18 @@ export function formatJsonPretty(jsonString: string): string {
|
|||
return jsonString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time as HH:MM:SS in 24-hour format
|
||||
*
|
||||
* @param date - Date object to format
|
||||
* @returns Formatted time string (HH:MM:SS)
|
||||
*/
|
||||
export function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import type { MCPClientConfig, MCPServerConfig, MCPServerSettingsEntry } from '$lib/types';
|
||||
import type { SettingsConfigType } from '$lib/types/settings';
|
||||
import type { McpServerOverride } from '$lib/types/database';
|
||||
import { MCPTransportType } from '$lib/enums';
|
||||
import { MCPTransportType, MCPLogLevel } from '$lib/enums';
|
||||
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
|
||||
import { normalizePositiveNumber } from '$lib/utils/number';
|
||||
import { Info, AlertTriangle, XCircle } from '@lucide/svelte';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
/**
|
||||
* Detects the MCP transport type from a URL.
|
||||
|
|
@ -251,3 +253,37 @@ export function buildMcpClientConfig(
|
|||
servers
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate icon component for a log level
|
||||
*
|
||||
* @param level - MCP log level
|
||||
* @returns Lucide icon component
|
||||
*/
|
||||
export function getMcpLogLevelIcon(level: MCPLogLevel): Component {
|
||||
switch (level) {
|
||||
case MCPLogLevel.Error:
|
||||
return XCircle;
|
||||
case MCPLogLevel.Warn:
|
||||
return AlertTriangle;
|
||||
default:
|
||||
return Info;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate CSS class for a log level
|
||||
*
|
||||
* @param level - MCP log level
|
||||
* @returns Tailwind CSS class string
|
||||
*/
|
||||
export function getMcpLogLevelClass(level: MCPLogLevel): string {
|
||||
switch (level) {
|
||||
case MCPLogLevel.Error:
|
||||
return 'text-destructive';
|
||||
case MCPLogLevel.Warn:
|
||||
return 'text-yellow-600 dark:text-yellow-500';
|
||||
default:
|
||||
return 'text-muted-foreground';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue