File size: 3,341 Bytes
52c6f5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<script lang="ts">
	import { mcpServers, type MCPServerEntity, type MCPFormData } from "$lib/state/mcps.svelte.js";
	import { projects } from "$lib/state/projects.svelte.js";
	import { extractDomain } from "$lib/utils/url.js";
	import IconEdit from "~icons/carbon/edit";
	import IconDelete from "~icons/carbon/trash-can";
	import Switch from "../switch.svelte";
	import McpForm from "./mcp-form.svelte";

	interface Props {
		server: MCPServerEntity;
	}

	let { server }: Props = $props();

	let editing = $state(false);

	async function deleteServer() {
		await mcpServers.delete(server.id);

		// Remove from project's enabled MCPs if it was enabled
		const currentProject = projects.current;
		if (!currentProject?.enabledMCPs?.includes(server.id)) return;
		await projects.update({
			...currentProject,
			enabledMCPs: currentProject.enabledMCPs.filter(mcpId => mcpId !== server.id),
		});
	}

	async function setEnabled(enabled: boolean) {
		const currentProject = projects.current;
		if (!currentProject) return;

		const enabledMCPs = currentProject.enabledMCPs || [];
		const newEnabledMCPs = enabled ? [...enabledMCPs, server.id] : enabledMCPs.filter(id => id !== server.id);

		await projects.update({
			...currentProject,
			enabledMCPs: newEnabledMCPs,
		});
	}

	const isEnabled = $derived(projects.current?.enabledMCPs?.includes(server.id) || false);

	async function saveServer(formData: MCPFormData) {
		await mcpServers.update({
			...server,
			...formData,
		});
		editing = false;
	}

	function getFaviconUrl(url: string): string {
		const domain = extractDomain(url);
		return `https://www.google.com/s2/favicons?domain=https://${domain}&sz=64`;
	}

	function urlWithoutSubpaths(url: string): string {
		const urlObj = new URL(url);
		return urlObj.origin;
	}
</script>

<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-700">
	<div class="flex justify-between">
		<div>
			<div class="flex items-center gap-1">
				<img src={getFaviconUrl(server.url)} alt="Server Icon" class="size-4 rounded p-0.5 dark:bg-gray-500" />
				<span class="font-bold">{server.name}</span>
			</div>
			<p class="mt-1 truncate text-sm dark:text-neutral-300">
				<span class="rounded bg-blue-900 px-0.75 py-0.25 uppercase">
					{server.protocol}
				</span>
				<span>
					{urlWithoutSubpaths(server.url)}
				</span>
			</p>
			{#if server.headers && Object.keys(server.headers).length > 0}
				<p class="mt-1 text-xs dark:text-neutral-400">
					Headers: {Object.keys(server.headers).length} configured
				</p>
			{/if}
		</div>
		<div class="flex flex-col items-end justify-between gap-2">
			<Switch bind:value={() => isEnabled, v => setEnabled(v)} />
			<div class="flex items-center gap-2">
				{#if !editing}
					<button class="btn-mini" onclick={() => (editing = true)}>
						<IconEdit class="h-4 w-4" />
						<span>Edit</span>
					</button>
					<button
						class="btn-mini text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
						onclick={() => deleteServer()}
					>
						<IconDelete class="h-4 w-4" />
						<span>Delete</span>
					</button>
				{/if}
			</div>
		</div>
	</div>

	{#if editing}
		<div class="mt-2 border-t border-neutral-500 pt-2 dark:border-neutral-700">
			<McpForm {server} onSubmit={saveServer} onCancel={() => (editing = false)} />
		</div>
	{/if}
</div>