McpConfigurationDisplay.tsx156 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { stringify as stringifyToml } from '@std/toml/stringify' |
| 4 | import yaml from 'js-yaml' |
| 5 | import { ExternalLink } from 'lucide-react' |
| 6 | import Image from 'next/image' |
| 7 | import { Button, cn } from 'ui' |
| 8 | import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock' |
| 9 | |
| 10 | import type { McpClient, McpClientConfig, McpOnCopyCallback } from '../types' |
| 11 | import { getMcpButtonData } from '../utils/getMcpButtonData' |
| 12 | |
| 13 | interface McpConfigurationDisplayProps { |
| 14 | selectedClient: McpClient |
| 15 | clientConfig: McpClientConfig |
| 16 | className?: string |
| 17 | theme?: 'light' | 'dark' |
| 18 | onCopyCallback: (type?: McpOnCopyCallback) => void |
| 19 | onInstallCallback?: () => void |
| 20 | isPlatform?: boolean |
| 21 | } |
| 22 | |
| 23 | type ConfigFormat = CodeBlockLang | 'toml' |
| 24 | |
| 25 | export function McpConfigurationDisplay({ |
| 26 | selectedClient, |
| 27 | clientConfig, |
| 28 | className, |
| 29 | theme = 'dark', |
| 30 | onCopyCallback, |
| 31 | onInstallCallback, |
| 32 | isPlatform, |
| 33 | }: McpConfigurationDisplayProps) { |
| 34 | const mcpButtonData = getMcpButtonData({ |
| 35 | theme, |
| 36 | client: selectedClient, |
| 37 | clientConfig, |
| 38 | isPlatform, |
| 39 | }) |
| 40 | |
| 41 | // Extract file extension and determine format |
| 42 | const fileExtension = selectedClient.configFile?.split('.').pop()?.toLowerCase() |
| 43 | // If the file extension is not 'json', 'yaml', or 'toml', default to 'txt' |
| 44 | let configFormat: ConfigFormat | undefined |
| 45 | if (['json', 'yaml', 'toml'].includes(fileExtension ?? '')) { |
| 46 | configFormat = fileExtension as ConfigFormat |
| 47 | } |
| 48 | |
| 49 | // Serialize config based on format |
| 50 | let configValue: string |
| 51 | switch (configFormat) { |
| 52 | case 'yaml': |
| 53 | configValue = yaml.dump(clientConfig, { indent: 2, lineWidth: -1 }) |
| 54 | break |
| 55 | case 'toml': |
| 56 | configValue = stringifyToml(clientConfig as Record<string, any>).trim() |
| 57 | break |
| 58 | case 'json': |
| 59 | configValue = JSON.stringify(clientConfig, null, 2) |
| 60 | break |
| 61 | default: |
| 62 | configValue = String(clientConfig) |
| 63 | } |
| 64 | |
| 65 | // Toml will default to undefined display language |
| 66 | const displayLanguage: CodeBlockLang | undefined = |
| 67 | configFormat === 'toml' ? undefined : configFormat |
| 68 | |
| 69 | return ( |
| 70 | <div className={cn('space-y-4', className)}> |
| 71 | {mcpButtonData && ( |
| 72 | <> |
| 73 | <div className="text-xs text-foreground-light"> |
| 74 | {selectedClient.deepLinkDescription ?? 'Install in one click:'} |
| 75 | </div> |
| 76 | <Button type="secondary" size="small" asChild> |
| 77 | <a |
| 78 | href={mcpButtonData.deepLink} |
| 79 | target="_blank" |
| 80 | rel="noopener noreferrer" |
| 81 | className="flex items-center gap-2 [&>span]:flex [&>span]:items-center [&>span]:gap-2" |
| 82 | onClick={onInstallCallback} |
| 83 | > |
| 84 | <Image |
| 85 | src={mcpButtonData.imageSrc} |
| 86 | alt={`${selectedClient.label} icon`} |
| 87 | width={16} |
| 88 | height={16} |
| 89 | className="shrink-0" |
| 90 | /> |
| 91 | Add to {selectedClient.label} |
| 92 | </a> |
| 93 | </Button> |
| 94 | </> |
| 95 | )} |
| 96 | |
| 97 | {selectedClient.primaryInstructions && |
| 98 | selectedClient.primaryInstructions(clientConfig, onCopyCallback, { isPlatform })} |
| 99 | |
| 100 | {selectedClient.configFile && ( |
| 101 | <> |
| 102 | <div className="text-xs text-foreground-light"> |
| 103 | {selectedClient.primaryInstructions |
| 104 | ? 'Alternatively, add' |
| 105 | : mcpButtonData |
| 106 | ? 'Or add' |
| 107 | : 'Add'}{' '} |
| 108 | this configuration to{' '} |
| 109 | <code className="px-1 py-0.5 bg-surface-200 rounded-sm"> |
| 110 | {selectedClient.configFile} |
| 111 | </code> |
| 112 | : |
| 113 | </div> |
| 114 | <CodeBlock |
| 115 | value={configValue} |
| 116 | language={displayLanguage} |
| 117 | className="max-h-64 overflow-y-auto" |
| 118 | focusable={false} |
| 119 | onCopyCallback={() => onCopyCallback?.('config')} |
| 120 | /> |
| 121 | </> |
| 122 | )} |
| 123 | |
| 124 | {selectedClient.alternateInstructions && |
| 125 | selectedClient.alternateInstructions(clientConfig, onCopyCallback, { isPlatform })} |
| 126 | |
| 127 | {(selectedClient.docsUrl || selectedClient.externalDocsUrl) && ( |
| 128 | <div className="flex items-center gap-2 text-xs text-foreground-light"> |
| 129 | <span>Need help?</span> |
| 130 | {selectedClient.docsUrl && ( |
| 131 | <a |
| 132 | href={selectedClient.docsUrl} |
| 133 | target="_blank" |
| 134 | rel="noopener noreferrer" |
| 135 | className="text-brand-link hover:underline inline-flex items-center" |
| 136 | > |
| 137 | View setup guide |
| 138 | <ExternalLink className="h-3 w-3 ml-1" /> |
| 139 | </a> |
| 140 | )} |
| 141 | {selectedClient.externalDocsUrl && ( |
| 142 | <a |
| 143 | href={selectedClient.externalDocsUrl} |
| 144 | target="_blank" |
| 145 | rel="noopener noreferrer" |
| 146 | className="text-brand-link hover:underline inline-flex items-center" |
| 147 | > |
| 148 | View {selectedClient.label} docs |
| 149 | <ExternalLink className="h-3 w-3 ml-1" /> |
| 150 | </a> |
| 151 | )} |
| 152 | </div> |
| 153 | )} |
| 154 | </div> |
| 155 | ) |
| 156 | } |