Skip to content

Commit 4c6a684

Browse files
author
Lasim
committed
feat(frontend): enhance validation for HTTP server configuration
1 parent a869274 commit 4c6a684

File tree

5 files changed

+67
-46
lines changed

5 files changed

+67
-46
lines changed

services/frontend/src/components/admin/mcp-catalog/McpServerAddFormWizard.vue

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,47 @@ const canProceedFromGitHub = computed(() => {
192192
})
193193
194194
const canProceedFromClaudeConfig = computed(() => {
195-
return formData.value.claudeConfig.claude_desktop_config &&
196-
Object.keys(formData.value.claudeConfig.claude_desktop_config).length > 0
195+
const config = formData.value.claudeConfig.claude_desktop_config as any
196+
197+
if (!config || Object.keys(config).length === 0) {
198+
return false
199+
}
200+
201+
// Check if this is an HTTP config (fake structure with 'remote-server')
202+
if (config.mcpServers && config.mcpServers['remote-server']) {
203+
const remoteServer = config.mcpServers['remote-server']
204+
// For HTTP: URL must be non-empty and valid
205+
if (!remoteServer.url || remoteServer.url.trim() === '') {
206+
return false
207+
}
208+
// Strict URL validation
209+
try {
210+
const parsedUrl = new URL(remoteServer.url)
211+
212+
// Must use HTTP or HTTPS
213+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
214+
return false
215+
}
216+
217+
// Must have a valid hostname (not empty, not just a single character)
218+
const hostname = parsedUrl.hostname
219+
if (!hostname || hostname.length < 2) {
220+
return false
221+
}
222+
223+
// For non-localhost, require at least one dot (domain.tld format)
224+
if (hostname !== 'localhost' && !hostname.includes('.')) {
225+
return false
226+
}
227+
228+
return true
229+
} catch {
230+
return false
231+
}
232+
}
233+
234+
// For stdio config: if we got here, it's valid (already validated in StdioServerInput)
235+
return true
197236
})
198237
199238
const canSubmit = computed(() => {

services/frontend/src/components/admin/mcp-catalog/TechnicalStep.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,7 @@ onUnmounted(() => {
860860
</div>
861861

862862
<!-- Configuration Preview (when valid) -->
863-
<div v-if="isValid" class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
863+
<div v-if="isValid && isStdioTransport" class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
864864
<dt class="text-sm/6 font-medium text-gray-900">{{ t('mcpCatalog.form.technical.claudeConfig.preview.title') }}</dt>
865865
<dd class="mt-1 sm:col-span-2 sm:mt-0">
866866
<div class="space-y-4">

services/frontend/src/components/admin/mcp-catalog/steps/ClaudeDesktopConfigStep/ClaudeConfigJsonInput.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const emit = defineEmits<{
1919
2020
const { t } = useI18n()
2121
22+
const hasContent = computed(() => props.modelValue.trim().length > 0)
2223
const statusIcon = computed(() => props.isValid ? CheckCircle : AlertCircle)
2324
const statusColor = computed(() => props.isValid ? 'text-green-600' : 'text-red-600')
2425
@@ -31,7 +32,7 @@ const handleFormat = () => {
3132
<div class="space-y-2">
3233
<div class="flex items-center justify-between">
3334
<Label for="claude-config">{{ t('mcpCatalog.form.claudeConfig.label') }}</Label>
34-
<div class="flex items-center gap-2">
35+
<div v-if="hasContent" class="flex items-center gap-2">
3536
<component :is="statusIcon" :class="['h-4 w-4', statusColor]" />
3637
<span :class="['text-sm', statusColor]">
3738
{{ isValid ? t('mcpCatalog.form.claudeConfig.validConfiguration') : t('mcpCatalog.form.claudeConfig.invalidConfiguration') }}

services/frontend/src/components/admin/mcp-catalog/steps/ClaudeDesktopConfigStep/HttpServerInput.vue

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22
import { ref, computed, watch } from 'vue'
33
import { Label } from '@/components/ui/label'
44
import { Input } from '@/components/ui/input'
5-
import { Alert, AlertDescription } from '@/components/ui/alert'
6-
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
75
import {
86
Select,
97
SelectContent,
108
SelectItem,
119
SelectTrigger,
1210
SelectValue,
1311
} from '@/components/ui/select'
14-
import { AlertCircle, CheckCircle, Link as LinkIcon } from 'lucide-vue-next'
12+
import { AlertCircle, CheckCircle } from 'lucide-vue-next'
1513
1614
interface Props {
1715
modelValue?: {
@@ -38,16 +36,31 @@ const isValid = ref(false)
3836
3937
// Validate URL
4038
const validateUrl = (url: string) => {
39+
// Allow empty URLs (optional field)
4140
if (!url.trim()) {
42-
return { isValid: false, error: 'URL is required' }
41+
return { isValid: false, error: null }
4342
}
4443
4544
try {
4645
const parsedUrl = new URL(url)
46+
47+
// Must use HTTP or HTTPS
4748
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
4849
return { isValid: false, error: 'URL must use HTTP or HTTPS protocol' }
4950
}
50-
return { isValid: true }
51+
52+
// Must have a valid hostname (not empty, not just a single character)
53+
const hostname = parsedUrl.hostname
54+
if (!hostname || hostname.length < 2) {
55+
return { isValid: false, error: 'Invalid hostname' }
56+
}
57+
58+
// For non-localhost, require at least one dot (domain.tld format)
59+
if (hostname !== 'localhost' && !hostname.includes('.')) {
60+
return { isValid: false, error: 'Invalid domain format' }
61+
}
62+
63+
return { isValid: true, error: null }
5164
} catch {
5265
return { isValid: false, error: 'Please enter a valid URL' }
5366
}
@@ -66,7 +79,7 @@ const emitUpdate = () => {
6679
type: transportType.value
6780
})
6881
} else {
69-
validationError.value = validation.error || 'Invalid URL'
82+
validationError.value = validation.error
7083
isValid.value = false
7184
7285
// Still emit for editing purposes
@@ -83,6 +96,7 @@ watch([urlInput, transportType], () => {
8396
}, { immediate: true })
8497
8598
// Computed properties
99+
const hasUrl = computed(() => urlInput.value.trim().length > 0)
86100
const statusIcon = computed(() => isValid.value ? CheckCircle : AlertCircle)
87101
const statusColor = computed(() => isValid.value ? 'text-green-600' : 'text-red-600')
88102
</script>
@@ -122,7 +136,7 @@ const statusColor = computed(() => isValid.value ? 'text-green-600' : 'text-red-
122136
<div class="space-y-2">
123137
<div class="flex items-center justify-between">
124138
<Label for="server-url">Server URL</Label>
125-
<div class="flex items-center gap-2">
139+
<div v-if="hasUrl" class="flex items-center gap-2">
126140
<component :is="statusIcon" :class="['h-4 w-4', statusColor]" />
127141
<span :class="['text-sm', statusColor]">
128142
{{ isValid ? 'Valid URL' : 'Invalid URL' }}
@@ -143,34 +157,5 @@ const statusColor = computed(() => isValid.value ? 'text-green-600' : 'text-red-
143157
</p>
144158
</div>
145159

146-
<!-- Validation Error -->
147-
<Alert v-if="validationError" variant="destructive">
148-
<AlertCircle class="h-4 w-4" />
149-
<AlertDescription>
150-
{{ validationError }}
151-
</AlertDescription>
152-
</Alert>
153-
154-
<!-- Configuration Preview -->
155-
<Card v-if="isValid" class="border-green-200 bg-green-50">
156-
<CardHeader>
157-
<CardTitle class="text-sm text-green-800">Configuration Preview</CardTitle>
158-
</CardHeader>
159-
<CardContent class="space-y-3">
160-
<div>
161-
<Label class="text-xs text-green-700">Transport Type</Label>
162-
<code class="ml-2 text-sm bg-green-100 px-2 py-1 rounded">
163-
{{ transportType === 'sse' ? 'SSE (Server-Sent Events)' : 'HTTP (Streamable)' }}
164-
</code>
165-
</div>
166-
<div>
167-
<Label class="text-xs text-green-700">Server URL</Label>
168-
<div class="flex items-center gap-2 mt-1">
169-
<LinkIcon class="h-4 w-4 text-green-600" />
170-
<code class="text-sm bg-green-100 px-2 py-1 rounded break-all">{{ urlInput }}</code>
171-
</div>
172-
</div>
173-
</CardContent>
174-
</Card>
175160
</div>
176161
</template>

services/frontend/src/components/admin/mcp-catalog/steps/ClaudeDesktopConfigStep/StdioServerInput.vue

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Alert, AlertDescription } from '@/components/ui/alert'
55
import { AlertCircle } from 'lucide-vue-next'
66
import ClaudeConfigJsonInput from './ClaudeConfigJsonInput.vue'
77
import ClaudeConfigPreview from './ClaudeConfigPreview.vue'
8-
import ClaudeConfigExamples from './ClaudeConfigExamples.vue'
98
109
interface Props {
1110
modelValue: {
@@ -33,7 +32,7 @@ const extractedEnvVars = ref<string[]>([])
3332
const validateJson = (jsonString: string) => {
3433
try {
3534
if (!jsonString.trim()) {
36-
return { isValid: false, error: t('mcpCatalog.form.claudeConfig.validation.required') }
35+
return { isValid: false, error: null }
3736
}
3837
3938
const parsed = JSON.parse(jsonString)
@@ -98,7 +97,7 @@ watch(jsonInput, (newValue) => {
9897
raw_json: newValue
9998
})
10099
} else {
101-
validationError.value = validation.error || 'Invalid configuration'
100+
validationError.value = validation.error
102101
isValid.value = false
103102
extractedServerName.value = ''
104103
extractedCommand.value = ''
@@ -157,8 +156,5 @@ const formatJson = () => {
157156
:is-url-based-server="false"
158157
:headers="[]"
159158
/>
160-
161-
<!-- Examples Component -->
162-
<ClaudeConfigExamples />
163159
</div>
164160
</template>

0 commit comments

Comments
 (0)