Skip to content

Commit aa34334

Browse files
committed
feat(frontend): add language and runtime detection for MCP server forms
1 parent 0978dc6 commit aa34334

File tree

8 files changed

+185
-8
lines changed

8 files changed

+185
-8
lines changed

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ClaudeDesktopConfigStep from '@/components/admin/mcp-catalog/ClaudeDeskto
1515
import ConfigurationSchemaStepAdd from '@/components/admin/mcp-catalog/steps/ConfigurationSchemaStepAdd.vue'
1616
import BasicInfoStepAdd from '@/components/admin/mcp-catalog/steps/BasicInfoStepAdd.vue'
1717
import type { McpServerFormData } from '@/views/admin/mcp-server-catalog/types'
18+
import { useRuntimeDetection } from '@/composables/admin/mcp-catalog'
1819
1920
// Props interface
2021
interface Props {
@@ -38,6 +39,7 @@ const emit = defineEmits<{
3839
3940
const { t } = useI18n()
4041
const eventBus = useEventBus()
42+
const { detectRuntimeFromCommand } = useRuntimeDetection()
4143
4244
// Form data interface for add wizard
4345
interface McpServerAddFormData {
@@ -68,6 +70,8 @@ interface McpServerAddFormData {
6870
transport_type: string
6971
website_url: string
7072
icon_url: string
73+
language: string
74+
runtime: string
7175
}
7276
}
7377
@@ -152,7 +156,9 @@ const formData = ref<McpServerAddFormData>({
152156
auto_install_new_default_team: false,
153157
transport_type: 'auto',
154158
website_url: '',
155-
icon_url: ''
159+
icon_url: '',
160+
language: 'typescript',
161+
runtime: 'node'
156162
}
157163
})
158164
@@ -276,6 +282,39 @@ const nextStep = () => {
276282
currentStep.value++
277283
const stepData = steps[currentStep.value]
278284
if (!stepData) return
285+
286+
// If moving to the Basic Info step (index 3), populate language and runtime from detected values
287+
if (currentStep.value === 3) {
288+
const claudeConfig = formData.value.claudeConfig.claude_desktop_config as any
289+
let serverConfig: any = null
290+
291+
if (claudeConfig && claudeConfig.mcpServers) {
292+
const serverKey = Object.keys(claudeConfig.mcpServers)[0]
293+
serverConfig = serverKey ? claudeConfig.mcpServers[serverKey] : null
294+
}
295+
296+
// Detect language and runtime
297+
let detectedLanguage = 'typescript'
298+
let detectedRuntime = 'node'
299+
300+
if (serverConfig) {
301+
if (serverConfig.url) {
302+
// HTTP/SSE servers
303+
detectedLanguage = 'http'
304+
detectedRuntime = 'http'
305+
} else if (serverConfig.command) {
306+
// STDIO servers - detect from command
307+
const detection = detectRuntimeFromCommand(serverConfig.command)
308+
detectedLanguage = detection.language
309+
detectedRuntime = detection.runtime
310+
}
311+
}
312+
313+
// Update form data with detected values
314+
formData.value.basic.language = detectedLanguage
315+
formData.value.basic.runtime = detectedRuntime
316+
}
317+
279318
emit('stepChanged', { step: currentStep.value, stepKey: stepData.key })
280319
281320
// Emit event bus event
@@ -391,10 +430,11 @@ const submitForm = async () => {
391430
let extractedPackages: any = null;
392431
let extractedRemotes: any = null;
393432
let extractedTransportType = 'stdio';
433+
let serverConfig: any = null;
394434
395435
if (claudeConfig && claudeConfig.mcpServers) {
396436
const serverKey = Object.keys(claudeConfig.mcpServers)[0];
397-
const serverConfig = serverKey ? claudeConfig.mcpServers[serverKey] : null;
437+
serverConfig = serverKey ? claudeConfig.mcpServers[serverKey] : null;
398438
399439
if (serverConfig) {
400440
if (serverConfig.url) {
@@ -422,6 +462,11 @@ const submitForm = async () => {
422462
}
423463
}
424464
465+
// Use language and runtime from the Basic Info form (user can edit these in step 4)
466+
// These values are auto-populated when navigating to step 4, but user can override them
467+
const finalLanguage = formData.value.basic.language || 'typescript'
468+
const finalRuntime = formData.value.basic.runtime || 'node'
469+
425470
// Construct the final payload for the backend API
426471
const finalPayload: any = {
427472
// Basic Info
@@ -445,8 +490,8 @@ const submitForm = async () => {
445490
claude_desktop_config: formData.value.claudeConfig.claude_desktop_config,
446491
447492
// Properly extracted fields based on transport type
448-
language: extractedTransportType === 'http' || extractedTransportType === 'sse' ? 'http' : 'typescript',
449-
runtime: extractedTransportType === 'http' || extractedTransportType === 'sse' ? 'http' : 'node',
493+
language: finalLanguage,
494+
runtime: finalRuntime,
450495
transport_type: extractedTransportType,
451496
packages: extractedPackages,
452497
remotes: extractedRemotes,

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,9 @@ const formData = ref<McpServerFormData>({
336336
featured: false,
337337
auto_install_new_default_team: false,
338338
website_url: '',
339-
icon_url: ''
339+
icon_url: '',
340+
language: 'typescript',
341+
runtime: 'node'
340342
},
341343
repository: {
342344
repository_url: '',
@@ -537,7 +539,9 @@ const autoPopulateFromRepository = (repositoryData: any) => {
537539
tags: repositoryData.topics || repositoryData.tags || [],
538540
featured: false,
539541
auto_install_new_default_team: false,
540-
website_url: repositoryData.homepage || repositoryData.website_url || ''
542+
website_url: repositoryData.homepage || repositoryData.website_url || '',
543+
language: formData.value.basic.language || 'typescript',
544+
runtime: formData.value.basic.runtime || 'node'
541545
},
542546
repository: {
543547
repository_url: repositoryData.html_url || repositoryData.repository_url || formData.value.repository_setup.repository_url,

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Switch } from '@/components/ui/switch'
2626
import { X, Plus, CheckCircle } from 'lucide-vue-next'
2727
import { DynamicIcon } from '@/components/ui/dynamic-icon'
2828
import type { BasicInfoFormData } from '@/views/admin/mcp-server-catalog/types'
29+
import { LANGUAGE_OPTIONS, RUNTIME_OPTIONS } from '@/views/admin/mcp-server-catalog/types'
2930
import { useCategories } from '@/composables/admin/mcp-catalog/useCategories'
3031
import { useTagManager } from '@/composables/admin/mcp-catalog/useTagManager'
3132
import SharedFormField from '../shared/SharedFormField.vue'
@@ -221,6 +222,58 @@ loadCategories()
221222
</div>
222223
</SharedFormField>
223224

225+
<!-- Language -->
226+
<SharedFormField
227+
label="Programming Language"
228+
description="The primary programming language used by this MCP server (auto-detected from command)"
229+
>
230+
<Select
231+
:model-value="localData.language"
232+
@update:model-value="(value) => updateField('language', String(value || 'typescript'))"
233+
>
234+
<SelectTrigger>
235+
<span>
236+
{{ LANGUAGE_OPTIONS.find(opt => opt.value === localData.language)?.label || 'Select language' }}
237+
</span>
238+
</SelectTrigger>
239+
<SelectContent>
240+
<SelectItem
241+
v-for="option in LANGUAGE_OPTIONS"
242+
:key="option.value"
243+
:value="option.value"
244+
>
245+
{{ option.label }}
246+
</SelectItem>
247+
</SelectContent>
248+
</Select>
249+
</SharedFormField>
250+
251+
<!-- Runtime -->
252+
<SharedFormField
253+
label="Runtime Environment"
254+
description="The runtime environment required to run this MCP server (auto-detected from command)"
255+
>
256+
<Select
257+
:model-value="localData.runtime"
258+
@update:model-value="(value) => updateField('runtime', String(value || 'node'))"
259+
>
260+
<SelectTrigger>
261+
<span>
262+
{{ RUNTIME_OPTIONS.find(opt => opt.value === localData.runtime)?.label || 'Select runtime' }}
263+
</span>
264+
</SelectTrigger>
265+
<SelectContent>
266+
<SelectItem
267+
v-for="option in RUNTIME_OPTIONS"
268+
:key="option.value"
269+
:value="option.value"
270+
>
271+
{{ option.label }}
272+
</SelectItem>
273+
</SelectContent>
274+
</Select>
275+
</SharedFormField>
276+
224277
<!-- Author Name -->
225278
<SharedFormField
226279
:label="t('mcpCatalog.form.basic.author.label')"

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ const defaultData: BasicInfoFormData = {
7070
featured: false,
7171
auto_install_new_default_team: false,
7272
website_url: '',
73-
icon_url: ''
73+
icon_url: '',
74+
language: 'typescript',
75+
runtime: 'node'
7476
}
7577
7678
// Storage-first reactive data - using ref instead of computed for better reactivity
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { useCategories } from './useCategories'
22
export { useTagManager } from './useTagManager'
33
export { useMcpCatalogServerCache } from './useMcpCatalogServerCache'
4+
export { useRuntimeDetection } from './useRuntimeDetection'
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Runtime Detection Composable
3+
*
4+
* Detects programming language and runtime environment from command-line commands.
5+
* Used when parsing Claude Desktop config or analyzing MCP server configurations.
6+
*
7+
* @example
8+
* ```typescript
9+
* import { useRuntimeDetection } from '@/composables/admin/mcp-catalog'
10+
*
11+
* const { detectRuntimeFromCommand } = useRuntimeDetection()
12+
* const result = detectRuntimeFromCommand('uvx') // { language: 'python', runtime: 'python' }
13+
* ```
14+
*/
15+
16+
export interface RuntimeDetection {
17+
language: string
18+
runtime: string
19+
}
20+
21+
export function useRuntimeDetection() {
22+
/**
23+
* Detects runtime and language from a command string
24+
*
25+
* @param command - The command to analyze (e.g., 'uvx', 'npx', 'python3')
26+
* @returns RuntimeDetection object with language and runtime
27+
*
28+
* @example
29+
* detectRuntimeFromCommand('uvx') // { language: 'python', runtime: 'python' }
30+
* detectRuntimeFromCommand('npx') // { language: 'typescript', runtime: 'node' }
31+
*/
32+
const detectRuntimeFromCommand = (command: string): RuntimeDetection => {
33+
// Normalize: lowercase, trim, extract base command from paths
34+
const normalizedCommand = command.toLowerCase().trim().split('/').pop() || command
35+
36+
// Node.js / TypeScript ecosystem
37+
if (['npx', 'npm', 'node', 'bun', 'deno', 'yarn', 'pnpm'].includes(normalizedCommand)) {
38+
return { language: 'typescript', runtime: 'node' }
39+
}
40+
41+
// Python ecosystem
42+
if (['python', 'python3', 'pip', 'pipx', 'uvx', 'uv', 'poetry', 'conda'].includes(normalizedCommand)) {
43+
return { language: 'python', runtime: 'python' }
44+
}
45+
46+
// Go
47+
if (normalizedCommand === 'go') {
48+
return { language: 'go', runtime: 'go' }
49+
}
50+
51+
// Rust
52+
if (normalizedCommand === 'cargo') {
53+
return { language: 'rust', runtime: 'rust' }
54+
}
55+
56+
// Ruby
57+
if (['ruby', 'gem', 'bundle'].includes(normalizedCommand)) {
58+
return { language: 'ruby', runtime: 'ruby' }
59+
}
60+
61+
// Default fallback to Node.js
62+
return { language: 'typescript', runtime: 'node' }
63+
}
64+
65+
return {
66+
detectRuntimeFromCommand
67+
}
68+
}

services/frontend/src/views/admin/mcp-server-catalog/edit/[id].vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,9 @@ const convertServerToFormData = (server: McpServer, readmeBase64: string = ''):
189189
featured: Boolean(server.featured),
190190
auto_install_new_default_team: Boolean(server.auto_install_new_default_team),
191191
website_url: server.website_url || '',
192-
icon_url: server.icon_url || ''
192+
icon_url: server.icon_url || '',
193+
language: server.language || 'typescript',
194+
runtime: server.runtime || 'node'
193195
},
194196
repository: {
195197
repository_url: server.repository_url || '',

services/frontend/src/views/admin/mcp-server-catalog/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ export interface BasicInfoFormData {
341341
auto_install_new_default_team: boolean
342342
website_url: string
343343
icon_url?: string
344+
language: string
345+
runtime: string
344346
}
345347

346348
export interface RepositoryFormData {

0 commit comments

Comments
 (0)