| <script lang="ts"> |
| import type { KeyValuePair } from "$lib/types/Tool"; |
| import { |
| validateMcpServerUrl, |
| validateHeader, |
| isSensitiveHeader, |
| } from "$lib/utils/mcpValidation"; |
| import IconEye from "~icons/carbon/view"; |
| import IconEyeOff from "~icons/carbon/view-off"; |
| import IconTrash from "~icons/carbon/trash-can"; |
| import IconAdd from "~icons/carbon/add"; |
| import IconWarning from "~icons/carbon/warning"; |
|
|
| interface Props { |
| onsubmit: (server: { name: string; url: string; headers?: KeyValuePair[] }) => void; |
| oncancel: () => void; |
| initialName?: string; |
| initialUrl?: string; |
| initialHeaders?: KeyValuePair[]; |
| submitLabel?: string; |
| } |
|
|
| let { |
| onsubmit, |
| oncancel, |
| initialName = "", |
| initialUrl = "", |
| initialHeaders = [], |
| submitLabel = "Add Server", |
| }: Props = $props(); |
|
|
| let name = $state(initialName); |
| let url = $state(initialUrl); |
| let headers = $state<KeyValuePair[]>(initialHeaders.length > 0 ? [...initialHeaders] : []); |
| let showHeaderValues = $state<Record<number, boolean>>({}); |
| let error = $state<string | null>(null); |
|
|
| function addHeader() { |
| headers = [...headers, { key: "", value: "" }]; |
| } |
|
|
| function removeHeader(index: number) { |
| headers = headers.filter((_, i) => i !== index); |
| delete showHeaderValues[index]; |
| } |
|
|
| function toggleHeaderVisibility(index: number) { |
| showHeaderValues = { |
| ...showHeaderValues, |
| [index]: !showHeaderValues[index], |
| }; |
| } |
|
|
| function validate(): boolean { |
| if (!name.trim()) { |
| error = "Server name is required"; |
| return false; |
| } |
|
|
| if (!url.trim()) { |
| error = "Server URL is required"; |
| return false; |
| } |
|
|
| const urlValidation = validateMcpServerUrl(url); |
| if (!urlValidation) { |
| error = "Invalid URL."; |
| return false; |
| } |
|
|
| |
| 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); |
| if (headerError) { |
| error = `Header ${i + 1}: ${headerError}`; |
| return false; |
| } |
| } |
| } |
|
|
| error = null; |
| return true; |
| } |
|
|
| function handleSubmit() { |
| if (!validate()) return; |
|
|
| |
| const filteredHeaders = headers.filter((h) => h.key.trim() && h.value.trim()); |
|
|
| onsubmit({ |
| name: name.trim(), |
| url: url.trim(), |
| headers: filteredHeaders.length > 0 ? filteredHeaders : undefined, |
| }); |
| } |
| </script> |
|
|
| <div class="space-y-4"> |
| |
| <div> |
| <label |
| for="server-name" |
| class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300" |
| > |
| Server Name <span class="text-red-500">*</span> |
| </label> |
| <input |
| id="server-name" |
| type="text" |
| bind:value={name} |
| placeholder="My MCP Server" |
| class="mt-1.5 w-full 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> |
| |
| |
| <div> |
| <label for="server-url" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"> |
| Server URL <span class="text-red-500">*</span> |
| </label> |
| <input |
| id="server-url" |
| type="url" |
| bind:value={url} |
| placeholder="https://example.com/mcp" |
| class="mt-1.5 w-full 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> |
| |
| |
| <details class="rounded-lg border border-gray-200 dark:border-gray-700"> |
| <summary class="cursor-pointer px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300"> |
| HTTP Headers (Optional) |
| </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"> |
| <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> |
| <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> |
| </div> |
| {/each} |
| {/if} |
| |
| <button |
| type="button" |
| onclick={addHeader} |
| class="flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" |
| > |
| <IconAdd class="size-4" /> |
| 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> |
| </div> |
| </details> |
| |
| |
| <div |
| class="rounded-lg border border-amber-200 bg-amber-50 p-3 text-amber-900 dark:border-yellow-900/40 dark:bg-yellow-900/20 dark:text-yellow-100" |
| > |
| <div class="flex items-start gap-3"> |
| <IconWarning class="mt-0.5 size-4 flex-none text-amber-600 dark:text-yellow-300" /> |
| <div class="text-sm leading-5"> |
| <p class="font-medium">Be careful with custom MCP servers.</p> |
| <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. |
| </p> |
| </div> |
| </div> |
| </div> |
| |
| |
| {#if error} |
| <div |
| class="rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20" |
| > |
| <p class="text-sm text-red-800 dark:text-red-200">{error}</p> |
| </div> |
| {/if} |
| |
| |
| <div class="flex justify-end gap-2"> |
| <button |
| type="button" |
| onclick={oncancel} |
| class="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" |
| > |
| Cancel |
| </button> |
| <button |
| type="button" |
| onclick={handleSubmit} |
| class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600" |
| > |
| {submitLabel} |
| </button> |
| </div> |
| </div> |
| |