Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 85 additions & 50 deletions src/lib/components/mcp/AddServerForm.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import type { KeyValuePair } from "$lib/types/Tool";
import type { HeaderEntry } from "$lib/types/Tool";
import {
validateMcpServerUrl,
validateHeader,
Expand All @@ -12,11 +12,12 @@
import IconWarning from "~icons/carbon/warning";

interface Props {
onsubmit: (server: { name: string; url: string; headers?: KeyValuePair[] }) => void;
onsubmit: (server: { name: string; url: string; headers?: HeaderEntry[] }) => void;
oncancel: () => void;
initialName?: string;
initialUrl?: string;
initialHeaders?: KeyValuePair[];
initialDescription?: string;
initialHeaders?: HeaderEntry[];
submitLabel?: string;
}

Expand All @@ -25,18 +26,21 @@
oncancel,
initialName = "",
initialUrl = "",
initialDescription = "",
initialHeaders = [],
submitLabel = "Add Server",
}: Props = $props();

let name = $state("");
let url = $state("");
let headers = $state<KeyValuePair[]>([]);
let headers = $state<HeaderEntry[]>([]);
let headersOpen = $state(false);

$effect.pre(() => {
name = initialName;
url = initialUrl;
headers = initialHeaders.length > 0 ? [...initialHeaders] : [];
headersOpen = initialHeaders.length > 0;
});
let showHeaderValues = $state<Record<number, boolean>>({});
let error = $state<string | null>(null);
Expand Down Expand Up @@ -75,12 +79,21 @@
}

// Validate headers
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
if (header.key.trim() || header.value.trim()) {
const headerError = validateHeader(header.key, header.value);
for (const header of headers) {
const { key, value, required } = header;
const keyInput = key.trim();
const valueInput = value.trim();
if (required && !valueInput) {
error = `"${keyInput}" is required`;
return false;
}
if (keyInput || valueInput) {
const headerError = validateHeader(key, value);
if (headerError) {
error = `Header ${i + 1}: ${headerError}`;
error =
keyInput && headerError === "Header value is required"
? `${keyInput} value is required`
: headerError;
return false;
}
}
Expand All @@ -99,12 +112,20 @@
onsubmit({
name: name.trim(),
url: url.trim(),
headers: filteredHeaders.length > 0 ? filteredHeaders : undefined,
headers:
filteredHeaders.length > 0
? filteredHeaders.map(({ key, value }) => ({ key, value }))
: undefined,
});
}
</script>

<div class="space-y-4">
<!-- Description (from registry) -->
{#if initialDescription}
<p class="mt-0.5 text-sm text-gray-700 dark:text-gray-300">{initialDescription}</p>
{/if}

<!-- Server Name -->
<div>
<label
Expand Down Expand Up @@ -140,51 +161,63 @@
</div>

<!-- HTTP Headers -->
<details class="rounded-lg border border-gray-200 dark:border-gray-700">
<details
class="rounded-lg border border-gray-200 dark:border-gray-700"
open={headersOpen}
ontoggle={(e) => (headersOpen = (e.currentTarget as HTMLDetailsElement).open)}
>
<summary class="cursor-pointer px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300">
HTTP Headers (Optional)
HTTP Headers {#if headers.some((h) => h.required)}<span class="text-red-500">*</span
>{:else}(Optional){/if}
</summary>
<div class="space-y-2 border-t border-gray-200 p-4 dark:border-gray-700">
{#if headers.length === 0}
<p class="text-sm text-gray-500 dark:text-gray-400">No headers configured</p>
{:else}
{#each headers as header, i}
<div class="flex gap-2">
<input
bind:value={header.key}
placeholder="Header name (e.g., Authorization)"
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
<div class="relative flex-1">
<div class="flex flex-col gap-1">
<div class="flex gap-2">
<input
bind:value={header.value}
type={showHeaderValues[i] ? "text" : "password"}
placeholder="Value"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
bind:value={header.key}
placeholder="Header name (e.g., Authorization)"
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
{#if isSensitiveHeader(header.key)}
<div class="relative flex-1">
<input
bind:value={header.value}
type={showHeaderValues[i] ? "text" : "password"}
placeholder="Value"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 pr-10 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white"
/>
{#if isSensitiveHeader(header.key)}
<button
type="button"
onclick={() => toggleHeaderVisibility(i)}
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
title={showHeaderValues[i] ? "Hide value" : "Show value"}
>
{#if showHeaderValues[i]}
<IconEyeOff class="size-4" />
{:else}
<IconEye class="size-4" />
{/if}
</button>
{/if}
</div>
{#if !header.required}
<button
type="button"
onclick={() => toggleHeaderVisibility(i)}
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
title={showHeaderValues[i] ? "Hide value" : "Show value"}
onclick={() => removeHeader(i)}
class="rounded-lg bg-red-100 p-2 text-red-600 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
title="Remove header"
>
{#if showHeaderValues[i]}
<IconEyeOff class="size-4" />
{:else}
<IconEye class="size-4" />
{/if}
<IconTrash class="size-4" />
</button>
{/if}
</div>
<button
type="button"
onclick={() => removeHeader(i)}
class="rounded-lg bg-red-100 p-2 text-red-600 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
title="Remove header"
>
<IconTrash class="size-4" />
</button>
{#if header.description}
<p class="pl-1 text-xs text-gray-500 dark:text-gray-400">{header.description}</p>
{/if}
</div>
{/each}
{/if}
Expand All @@ -198,15 +231,17 @@
Add Header
</button>

<p class="text-xs text-gray-500 dark:text-gray-400">
Common examples:<br />
• Bearer token:
<code class="rounded bg-gray-100 px-1 dark:bg-gray-700"
>Authorization: Bearer YOUR_TOKEN</code
><br />
• API key:
<code class="rounded bg-gray-100 px-1 dark:bg-gray-700">X-API-Key: YOUR_KEY</code>
</p>
{#if !headers.some((h) => h.required)}
<p class="text-xs text-gray-500 dark:text-gray-400">
Common examples:<br />
• Bearer token:
<code class="rounded bg-gray-100 px-1 dark:bg-gray-700"
>Authorization: Bearer YOUR_TOKEN</code
><br />
• API key:
<code class="rounded bg-gray-100 px-1 dark:bg-gray-700">X-API-Key: YOUR_KEY</code>
</p>
{/if}
</div>
</details>

Expand All @@ -221,7 +256,7 @@
<p class="mt-1 text-[13px] text-amber-800 dark:text-yellow-100/90">
They receive your requests (including conversation context and any headers you add) and
can run powerful tools on your behalf. Only add servers you trust and review their source.
Never share confidental informations.
Never share confidential information.
</p>
</div>
</div>
Expand Down
Loading