|
| 1 | +import { Box, Flex, Text, Button } from '@backstage/ui'; |
| 2 | +import { RiServerLine } from '@remixicon/react'; |
| 3 | +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; |
| 4 | +import { useMcpCatalog } from '../../hooks'; |
| 5 | +import type { McpCatalogEntry } from '@julianpedro/plugin-dev-ai-hub-common'; |
| 6 | +import { devAiHubTranslationRef } from '../../translation'; |
| 7 | +import styles from './McpCatalogPage.module.css'; |
| 8 | + |
| 9 | +function CatalogEntryCard({ entry }: { entry: McpCatalogEntry }) { |
| 10 | + const { t } = useTranslationRef(devAiHubTranslationRef); |
| 11 | + const handleInstallVscode = () => { |
| 12 | + const config: Record<string, unknown> = { name: entry.id, type: entry.type }; |
| 13 | + if (entry.type === 'http') config.url = entry.url; |
| 14 | + if (entry.type === 'stdio') { |
| 15 | + config.command = entry.command; |
| 16 | + if (entry.args?.length) config.args = entry.args; |
| 17 | + if (entry.env && Object.keys(entry.env).length) config.env = entry.env; |
| 18 | + } |
| 19 | + window.location.href = `vscode:mcp/install?${encodeURIComponent(JSON.stringify(config))}`; |
| 20 | + }; |
| 21 | + |
| 22 | + const handleInstallCursor = () => { |
| 23 | + const config: Record<string, unknown> = { type: entry.type }; |
| 24 | + if (entry.type === 'http') config.url = entry.url; |
| 25 | + if (entry.type === 'stdio') { |
| 26 | + config.command = entry.command; |
| 27 | + if (entry.args?.length) config.args = entry.args; |
| 28 | + if (entry.env && Object.keys(entry.env).length) config.env = entry.env; |
| 29 | + } |
| 30 | + window.location.href = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(entry.name)}&config=${btoa(JSON.stringify(config))}`; |
| 31 | + }; |
| 32 | + |
| 33 | + const canInstall = |
| 34 | + (entry.type === 'http' && !!entry.url) || |
| 35 | + (entry.type === 'stdio' && !!entry.command); |
| 36 | + |
| 37 | + return ( |
| 38 | + <Box className={styles.catalogCard}> |
| 39 | + <Flex align="center" style={{ gap: 'var(--bui-space-2)' }}> |
| 40 | + <Box className={styles.catalogIcon}> |
| 41 | + {entry.icon ? ( |
| 42 | + <img |
| 43 | + src={entry.icon} |
| 44 | + alt={entry.name} |
| 45 | + className={styles.catalogIconImage} |
| 46 | + onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} |
| 47 | + /> |
| 48 | + ) : ( |
| 49 | + <RiServerLine size={20} style={{ color: 'var(--bui-fg-secondary)' }} /> |
| 50 | + )} |
| 51 | + </Box> |
| 52 | + <Box style={{ minWidth: 0, flex: 1 }}> |
| 53 | + <Text variant="body-small" weight="bold">{entry.name}</Text> |
| 54 | + <Text variant="body-x-small" color="secondary">{entry.type.toUpperCase()}</Text> |
| 55 | + </Box> |
| 56 | + </Flex> |
| 57 | + {entry.description && ( |
| 58 | + <Text variant="body-x-small" color="secondary" className={styles.catalogDescription}> |
| 59 | + {entry.description} |
| 60 | + </Text> |
| 61 | + )} |
| 62 | + <Flex style={{ gap: 'var(--bui-space-2)', marginTop: 'auto' }}> |
| 63 | + <Button size="small" variant="secondary" isDisabled={!canInstall} onPress={handleInstallVscode}> |
| 64 | + {t('mcpConfigDialog.catalogVscode')} |
| 65 | + </Button> |
| 66 | + <Button size="small" variant="secondary" isDisabled={!canInstall} onPress={handleInstallCursor}> |
| 67 | + {t('mcpConfigDialog.catalogCursor')} |
| 68 | + </Button> |
| 69 | + </Flex> |
| 70 | + </Box> |
| 71 | + ); |
| 72 | +} |
| 73 | + |
| 74 | +export function McpCatalogPage() { |
| 75 | + const { t } = useTranslationRef(devAiHubTranslationRef); |
| 76 | + const { catalog } = useMcpCatalog(); |
| 77 | + |
| 78 | + return ( |
| 79 | + <div className={styles.pageRoot}> |
| 80 | + <Flex align="center" style={{ gap: 'var(--bui-space-2)' }}> |
| 81 | + <RiServerLine size={20} style={{ color: 'var(--bui-fg-secondary)' }} /> |
| 82 | + <Text variant="title-small" weight="bold">{t('mcpConfigDialog.catalogTab')}</Text> |
| 83 | + {catalog.length > 0 && ( |
| 84 | + <Text variant="body-x-small" color="secondary"> |
| 85 | + {t('mcpConfigDialog.catalogAvailable', { n: String(catalog.length) })} |
| 86 | + </Text> |
| 87 | + )} |
| 88 | + </Flex> |
| 89 | + <Text variant="body-small" color="secondary" className={styles.subtitle}> |
| 90 | + {t('mcpConfigDialog.catalogDescription')} |
| 91 | + </Text> |
| 92 | + |
| 93 | + {catalog.length === 0 ? ( |
| 94 | + <Flex direction="column" align="center" className={styles.catalogEmpty}> |
| 95 | + <RiServerLine size={32} style={{ color: 'var(--bui-fg-secondary)' }} /> |
| 96 | + <Text variant="body-small" weight="bold">{t('mcpConfigDialog.catalogEmpty')}</Text> |
| 97 | + <Text variant="body-x-small" color="secondary">{t('mcpConfigDialog.catalogAddHint')}</Text> |
| 98 | + </Flex> |
| 99 | + ) : ( |
| 100 | + <div className={styles.catalogGrid}> |
| 101 | + {catalog.map(entry => ( |
| 102 | + <CatalogEntryCard key={entry.id} entry={entry} /> |
| 103 | + ))} |
| 104 | + </div> |
| 105 | + )} |
| 106 | + </div> |
| 107 | + ); |
| 108 | +} |
0 commit comments