Skip to content

Commit 54bf418

Browse files
author
Lasim
committed
feat(frontend): replace device_name with device_id in user configuration
1 parent f66d3d6 commit 54bf418

File tree

4 files changed

+106
-126
lines changed

4 files changed

+106
-126
lines changed

services/backend/src/services/mcpUserConfigurationService.ts

Lines changed: 4 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { eq, and, desc } from 'drizzle-orm';
3-
import { mcpUserConfigurations, mcpServerInstallations, mcpServers, authUser } from '../db/schema.sqlite';
3+
import { mcpUserConfigurations, mcpServerInstallations, mcpServers } from '../db/schema.sqlite';
44
import type { AnyDatabase } from '../db';
55
import type { FastifyBaseLogger } from 'fastify';
66
import { nanoid } from 'nanoid';
@@ -237,82 +237,17 @@ export class McpUserConfigurationService {
237237
}
238238
}
239239

240-
// Get user information for auto-populating environment variables
241-
const userInfo = await this.db
242-
.select({
243-
username: authUser.username,
244-
email: authUser.email
245-
})
246-
.from(authUser)
247-
.where(eq(authUser.id, userId))
248-
.limit(1);
249-
250-
const user = userInfo[0];
251-
if (!user) {
252-
throw new Error('User not found');
253-
}
254-
255-
// Auto-populate required environment variables based on server schema
240+
// Validate user args and env against server schema
256241
const serverInfo = installation[0].server;
257-
let processedUserEnv = data.user_env ? { ...data.user_env } : {};
258-
259-
this.logger.debug({
260-
operation: 'create_user_configuration_debug',
261-
originalUserEnv: data.user_env,
262-
processedUserEnv,
263-
serverInfo: serverInfo ? 'present' : 'missing',
264-
userInfo: { username: user.username, email: user.email }
265-
}, 'Debug: Starting environment variable processing');
266-
267242
if (serverInfo) {
268-
const envSchema = this.parseJsonField(serverInfo.user_env_schema, []);
269-
270-
this.logger.debug({
271-
operation: 'env_schema_debug',
272-
envSchema
273-
}, 'Debug: Environment schema parsed');
274-
275-
// Auto-populate required environment variables
276-
envSchema.forEach((envVar: any) => {
277-
this.logger.debug({
278-
operation: 'env_var_check',
279-
envVarName: envVar.name,
280-
required: envVar.required,
281-
currentValue: processedUserEnv[envVar.name],
282-
hasCurrentValue: !!processedUserEnv[envVar.name]
283-
}, 'Debug: Checking environment variable');
284-
285-
if (envVar.required && !processedUserEnv[envVar.name]) {
286-
// Auto-populate based on variable name and description
287-
if (envVar.name === 'from_user') {
288-
// Use username or email for from_user field
289-
processedUserEnv[envVar.name] = user.username || user.email;
290-
this.logger.info({
291-
operation: 'auto_populate_env_var',
292-
envVarName: envVar.name,
293-
value: processedUserEnv[envVar.name]
294-
}, 'Auto-populated required environment variable');
295-
}
296-
}
297-
});
298-
299-
this.logger.debug({
300-
operation: 'final_env_debug',
301-
processedUserEnv
302-
}, 'Debug: Final processed environment variables');
303-
304-
// Validate user args and env against server schema
305243
if (data.user_args) {
306244
this.validateUserArgs(data.user_args, this.parseJsonField(serverInfo.user_args_schema, []));
307245
}
308-
if (processedUserEnv) {
309-
this.validateUserEnv(processedUserEnv, envSchema);
246+
if (data.user_env) {
247+
this.validateUserEnv(data.user_env, this.parseJsonField(serverInfo.user_env_schema, []));
310248
}
311249
}
312250

313-
// Update data with processed environment variables
314-
data.user_env = processedUserEnv;
315-
316251
const configId = nanoid();
317252
const now = new Date();
318253

services/frontend/src/components/mcp-server/installation/UserConfiguration.vue

Lines changed: 90 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@
44
import { ref, computed, onMounted } from 'vue'
55
import { useI18n } from 'vue-i18n'
66
import { toast } from 'vue-sonner'
7-
import { Settings, Eye, EyeOff, Plus, Trash2 } from 'lucide-vue-next'
7+
import { Settings, Eye, EyeOff, Plus, Trash2, Monitor } from 'lucide-vue-next'
88
import { McpCatalogService } from '@/services/mcpCatalogService'
99
import { McpInstallationService } from '@/services/mcpInstallationService'
10+
import { useDevices } from '@/composables/useDevices'
1011
import { Button } from '@/components/ui/button'
1112
import { Badge } from '@/components/ui/badge'
1213
import { Input } from '@/components/ui/input'
1314
import { Label } from '@/components/ui/label'
1415
import { Textarea } from '@/components/ui/textarea'
16+
import {
17+
Select,
18+
SelectContent,
19+
SelectItem,
20+
SelectTrigger,
21+
SelectValue,
22+
} from '@/components/ui/select'
1523
import {
1624
AlertDialog,
1725
AlertDialogContent,
@@ -60,20 +68,21 @@ const showPassword = ref(false)
6068
const isSubmitting = ref(false)
6169
const formErrors = ref<Record<string, string>>({})
6270
63-
// Device name for new configurations
64-
const deviceName = ref('')
71+
// Device management
72+
const { devices, fetchDevices, isLoading: isLoadingDevices } = useDevices()
73+
const selectedDeviceId = ref('')
6574
6675
// Load server data and user configurations
6776
onMounted(async () => {
6877
try {
6978
isLoadingServer.value = true
7079
isLoadingUserConfig.value = true
71-
80+
7281
// Load server schema data
7382
if (props.installation.server_id) {
7483
serverData.value = await McpCatalogService.getServerById(props.installation.server_id)
7584
}
76-
85+
7786
// Load user configurations
7887
await loadUserConfigurations()
7988
} catch (error) {
@@ -92,7 +101,7 @@ const loadUserConfigurations = async () => {
92101
props.installation.id
93102
)
94103
userConfigurations.value = configs
95-
104+
96105
// Set current config (first one for now, could be device-specific later)
97106
if (configs.length > 0 && configs[0]) {
98107
currentUserConfig.value = configs[0]
@@ -171,7 +180,7 @@ const isLoading = computed(() => {
171180
// Modal functions
172181
const openEditModal = (item: any, type: 'arg' | 'env') => {
173182
if (!props.canEdit) return
174-
183+
175184
editingItem.value = item
176185
editingType.value = type
177186
editingValue.value = item.currentValue
@@ -180,10 +189,12 @@ const openEditModal = (item: any, type: 'arg' | 'env') => {
180189
isEditModalOpen.value = true
181190
}
182191
183-
const openCreateModal = () => {
192+
const openCreateModal = async () => {
184193
if (!props.canEdit) return
185-
186-
deviceName.value = ''
194+
195+
// Load devices when opening create modal
196+
await fetchDevices()
197+
selectedDeviceId.value = ''
187198
formErrors.value = {}
188199
isCreateModalOpen.value = true
189200
}
@@ -199,7 +210,7 @@ const closeEditModal = () => {
199210
200211
const closeCreateModal = () => {
201212
isCreateModalOpen.value = false
202-
deviceName.value = ''
213+
selectedDeviceId.value = ''
203214
formErrors.value = {}
204215
}
205216
@@ -222,63 +233,63 @@ const isTextarea = (item: any) => {
222233
223234
const validateForm = () => {
224235
const errors: Record<string, string> = {}
225-
236+
226237
if (editingItem.value?.required && !editingValue.value.trim()) {
227238
errors.value = t('mcpInstallations.userConfiguration.editModal.validation.required')
228239
}
229-
240+
230241
formErrors.value = errors
231242
return Object.keys(errors).length === 0
232243
}
233244
234245
const validateCreateForm = () => {
235246
const errors: Record<string, string> = {}
236-
237-
if (!deviceName.value.trim()) {
238-
errors.deviceName = t('mcpInstallations.userConfiguration.createModal.validation.deviceNameRequired')
247+
248+
if (!selectedDeviceId.value) {
249+
errors.deviceId = t('mcpInstallations.userConfiguration.createModal.validation.deviceRequired')
239250
}
240-
251+
241252
formErrors.value = errors
242253
return Object.keys(errors).length === 0
243254
}
244255
245256
const handleEdit = async () => {
246257
if (!validateForm() || !currentUserConfig.value) return
247-
258+
248259
isSubmitting.value = true
249-
260+
250261
try {
251262
let updatedArgs = [...currentUserArgs.value]
252263
let updatedEnv = { ...currentUserEnv.value }
253-
264+
254265
if (editingType.value === 'arg') {
255266
updatedArgs[editingItem.value.index] = editingValue.value
256267
} else {
257268
updatedEnv[editingItem.value.name] = editingValue.value
258269
}
259-
270+
260271
const updatedConfig: UserConfiguration = await McpInstallationService.updateUserConfiguration(
261272
props.teamId,
262273
props.installation.id,
263274
currentUserConfig.value.id,
264275
{
265-
device_name: currentUserConfig.value.device_name,
276+
device_id: currentUserConfig.value.device_id,
266277
user_args: updatedArgs,
267278
user_env: updatedEnv
268279
}
269280
)
270-
281+
271282
currentUserConfig.value = updatedConfig
272283
emit('configuration-updated', updatedConfig)
273-
274-
const itemName = editingType.value === 'arg'
284+
285+
const itemName = editingType.value === 'arg'
275286
? t('mcpInstallations.userConfiguration.table.values.argumentNumber', { number: editingItem.value.index + 1 })
276287
: editingItem.value.name
277-
288+
278289
toast.success(t('mcpInstallations.userConfiguration.editModal.success.updated', { item: itemName }), {
279290
description: t('mcpInstallations.userConfiguration.editModal.success.description')
280291
})
281-
292+
282293
closeEditModal()
283294
} catch (error) {
284295
console.error('Error updating user configuration:', error)
@@ -290,36 +301,50 @@ const handleEdit = async () => {
290301
291302
const handleCreate = async () => {
292303
if (!validateCreateForm()) return
293-
304+
294305
isSubmitting.value = true
295-
306+
296307
try {
297308
// Initialize with empty arrays/objects based on schema
298309
const initialArgs = new Array(userArgsSchema.value.length).fill('')
299310
const initialEnv: Record<string, string> = {}
300-
311+
312+
// Only include non-empty environment variables
301313
userEnvSchema.value.forEach((envSchema: any) => {
302-
initialEnv[envSchema.name] = ''
314+
// Don't add empty values to avoid validation errors
315+
if (envSchema.required) {
316+
// For required fields, we'll let the backend validation handle it
317+
initialEnv[envSchema.name] = ''
318+
}
319+
// For optional fields, we simply don't include them if empty
320+
})
321+
322+
// Filter out empty environment variables to avoid backend validation errors
323+
const filteredEnv: Record<string, string> = {}
324+
Object.entries(initialEnv).forEach(([key, value]) => {
325+
if (value && value.trim() !== '') {
326+
filteredEnv[key] = value
327+
}
303328
})
304-
329+
305330
const newConfig: UserConfiguration = await McpInstallationService.createUserConfiguration(
306331
props.teamId,
307332
props.installation.id,
308333
{
309-
device_name: deviceName.value.trim(),
310-
user_args: initialArgs,
311-
user_env: initialEnv
334+
device_id: selectedDeviceId.value,
335+
user_args: initialArgs.filter(arg => arg.trim() !== ''),
336+
user_env: filteredEnv
312337
}
313338
)
314-
339+
315340
currentUserConfig.value = newConfig
316341
userConfigurations.value.push(newConfig)
317342
emit('configuration-updated', newConfig)
318-
343+
319344
toast.success(t('mcpInstallations.userConfiguration.createModal.success.created'), {
320345
description: t('mcpInstallations.userConfiguration.createModal.success.description')
321346
})
322-
347+
323348
closeCreateModal()
324349
} catch (error) {
325350
console.error('Error creating user configuration:', error)
@@ -331,7 +356,7 @@ const handleCreate = async () => {
331356
332357
const modalTitle = computed(() => {
333358
if (!editingItem.value) return ''
334-
359+
335360
if (editingType.value === 'arg') {
336361
return t('mcpInstallations.userConfiguration.editModal.titleArg', { number: editingItem.value.index + 1 })
337362
} else {
@@ -649,21 +674,35 @@ const modalTitle = computed(() => {
649674
{{ formErrors.general }}
650675
</div>
651676

652-
<!-- Device Name Input -->
677+
<!-- Device Selection -->
653678
<div class="space-y-2">
654-
<Label for="device-name">{{ t('mcpInstallations.userConfiguration.createModal.form.labels.deviceName') }}</Label>
655-
<Input
656-
id="device-name"
657-
v-model="deviceName"
658-
:placeholder="t('mcpInstallations.userConfiguration.createModal.form.placeholders.deviceName')"
659-
:class="{ 'border-destructive': formErrors.deviceName }"
660-
required
661-
/>
662-
<div v-if="formErrors.deviceName" class="text-sm text-destructive">
663-
{{ formErrors.deviceName }}
679+
<Label for="device-select">{{ t('mcpInstallations.userConfiguration.createModal.form.labels.device') }}</Label>
680+
<Select v-model="selectedDeviceId">
681+
<SelectTrigger
682+
id="device-select"
683+
:class="{ 'border-destructive': formErrors.deviceId }"
684+
>
685+
<SelectValue :placeholder="isLoadingDevices ? t('mcpInstallations.userConfiguration.createModal.form.placeholders.loadingDevices') : t('mcpInstallations.userConfiguration.createModal.form.placeholders.selectDevice')" />
686+
</SelectTrigger>
687+
<SelectContent>
688+
<SelectItem
689+
v-for="device in devices"
690+
:key="device.id"
691+
:value="device.id"
692+
>
693+
<div class="flex items-center gap-2">
694+
<Monitor class="h-4 w-4 text-muted-foreground" />
695+
<span>{{ device.device_name }}</span>
696+
<span class="text-muted-foreground text-sm">({{ device.os_type }})</span>
697+
</div>
698+
</SelectItem>
699+
</SelectContent>
700+
</Select>
701+
<div v-if="formErrors.deviceId" class="text-sm text-destructive">
702+
{{ formErrors.deviceId }}
664703
</div>
665704
<p class="text-xs text-gray-500">
666-
{{ t('mcpInstallations.userConfiguration.createModal.form.help.deviceName') }}
705+
{{ t('mcpInstallations.userConfiguration.createModal.form.help.device') }}
667706
</p>
668707
</div>
669708

0 commit comments

Comments
 (0)