Spaces:
Runtime error
Runtime error
File size: 5,137 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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
<script lang="ts">
import { type MCPProtocol, type MCPServerEntity, type MCPFormData } from "$lib/state/mcps.svelte.js";
import { createFieldValidation } from "$lib/utils/form.svelte";
import { entries } from "$lib/utils/object.svelte";
import { isValidURL } from "$lib/utils/url.js";
import IconAdd from "~icons/carbon/add";
import IconCheck from "~icons/carbon/checkmark";
import IconDelete from "~icons/carbon/trash-can";
interface Props {
server?: MCPServerEntity;
onSubmit: (formData: MCPFormData) => Promise<void>;
onCancel: () => void;
submitLabel?: string;
}
let { server, onSubmit, onCancel, submitLabel = "Save" }: Props = $props();
let formState = $state({
name: server?.name || "",
url: server?.url || "",
protocol: (server?.protocol || "sse") as MCPProtocol,
headers: entries(server?.headers || {}),
});
const protocolOptions: MCPProtocol[] = ["sse", "http"];
const nameField = createFieldValidation({
validate: v => {
if (!v) return "Server name is required";
if (v.trim().length === 0) return "Server name cannot be empty";
},
});
const urlField = createFieldValidation({
validate: v => {
if (!v) return "Server URL is required";
if (v.trim().length === 0) return "Server URL cannot be empty";
if (!isValidURL(v)) return "Invalid URL";
},
});
const disabled = $derived(!nameField.valid || !urlField.valid);
async function handleSubmit(e: SubmitEvent) {
e.preventDefault();
if (!nameField.valid || !urlField.valid) return;
await onSubmit({
...formState,
headers: formState.headers.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
});
}
</script>
<form class="space-y-3" onsubmit={handleSubmit}>
<label class="flex flex-col gap-2">
<p class="block text-sm font-medium text-gray-900 dark:text-white">
Server Name <span class="text-red-800 dark:text-red-300">*</span>
</p>
<input
type="text"
bind:value={formState.name}
class="input block w-full"
placeholder="My MCP Server"
{...nameField.attrs}
required
/>
<p class="text-xs text-red-300">{nameField.msg}</p>
</label>
<label class="flex flex-col gap-2">
<p class="block text-sm font-medium text-gray-900 dark:text-white">
Server URL <span class="text-red-800 dark:text-red-300">*</span>
</p>
<input
type="url"
bind:value={formState.url}
class="input block w-full"
placeholder="https://mcp.example.com/sse"
{...urlField.attrs}
required
/>
<p class="text-xs text-red-300">{urlField.msg}</p>
</label>
<div class="flex flex-col gap-2">
<p class="block text-sm font-medium text-gray-900 dark:text-white">Protocol</p>
<div class="flex rounded-lg bg-gray-100 p-1 dark:bg-gray-700" role="radiogroup" aria-label="Server Protocol">
{#each protocolOptions as protocol}
<label class="relative flex-1 cursor-pointer">
<input
type="radio"
name="protocol-option"
value={protocol}
bind:group={formState.protocol}
class="peer sr-only"
/>
<div
class="flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 transition-colors duration-200 ease-in-out peer-checked:bg-white peer-checked:text-gray-900 peer-checked:shadow dark:text-gray-300 dark:peer-checked:bg-gray-800 dark:peer-checked:text-white"
>
{protocol.toUpperCase()}
</div>
<span
aria-hidden="true"
class="absolute inset-0 z-0 rounded-md transition-all duration-200 ease-in-out peer-focus:ring-2 peer-focus:ring-blue-500 peer-focus:ring-offset-2 peer-focus:ring-offset-gray-100 dark:peer-focus:ring-offset-gray-700"
></span>
</label>
{/each}
</div>
</div>
<div class="flex flex-col gap-2">
<p class="block text-sm font-medium text-gray-900 dark:text-white">Headers</p>
{#each formState.headers || [] as _, i (i)}
<div class="flex items-center gap-2">
<input
type="text"
bind:value={formState.headers[i]![0]}
class="flex-1 rounded-md border border-gray-300 bg-gray-50 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
placeholder="Header name"
/>
<span class="text-gray-500">:</span>
<input
type="text"
bind:value={formState.headers[i]![1]}
class="flex-1 rounded-md border border-gray-300 bg-gray-50 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
placeholder="Header value"
/>
<button
class="btn-sm !h-auto self-stretch text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
onclick={() => {
formState.headers.splice(i, 1);
}}
type="button"
>
<IconDelete class="h-4 w-4" />
</button>
</div>
{/each}
<button
class="btn-sm self-start"
type="button"
onclick={() => {
formState.headers.push(["", ""]);
formState = formState;
}}
>
<IconAdd class="size-4" />
Add Header
</button>
</div>
<div class="flex items-center gap-2">
<button class="btn-sm" {disabled}>
<IconCheck /><span>{submitLabel}</span>
</button>
<button class="btn-sm" type="button" onclick={onCancel}> Cancel </button>
</div>
</form>
|