McpConfigurationDisplay.tsx156 lines · main
1'use client'
2
3import { stringify as stringifyToml } from '@std/toml/stringify'
4import yaml from 'js-yaml'
5import { ExternalLink } from 'lucide-react'
6import Image from 'next/image'
7import { Button, cn } from 'ui'
8import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock'
9
10import type { McpClient, McpClientConfig, McpOnCopyCallback } from '../types'
11import { getMcpButtonData } from '../utils/getMcpButtonData'
12
13interface 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
23type ConfigFormat = CodeBlockLang | 'toml'
24
25export 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}