@samanhappy/mcphub 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/.eslintrc.json +25 -0
- package/.github/workflows/build.yml +51 -0
- package/.github/workflows/release.yml +19 -0
- package/.prettierrc +7 -0
- package/Dockerfile +51 -0
- package/assets/amap-edit.png +0 -0
- package/assets/amap-result.png +0 -0
- package/assets/cherry-mcp.png +0 -0
- package/assets/cursor-mcp.png +0 -0
- package/assets/cursor-query.png +0 -0
- package/assets/cursor-tools.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/dashboard.zh.png +0 -0
- package/assets/group.png +0 -0
- package/assets/group.zh.png +0 -0
- package/assets/market.zh.png +0 -0
- package/assets/wegroup.jpg +0 -0
- package/assets/wegroup.png +0 -0
- package/assets/wexin.png +0 -0
- package/doc/intro.md +73 -0
- package/doc/intro2.md +232 -0
- package/entrypoint.sh +10 -0
- package/frontend/favicon.ico +0 -0
- package/frontend/index.html +13 -0
- package/frontend/postcss.config.js +6 -0
- package/frontend/src/App.tsx +44 -0
- package/frontend/src/components/AddGroupForm.tsx +132 -0
- package/frontend/src/components/AddServerForm.tsx +90 -0
- package/frontend/src/components/ChangePasswordForm.tsx +158 -0
- package/frontend/src/components/EditGroupForm.tsx +149 -0
- package/frontend/src/components/EditServerForm.tsx +76 -0
- package/frontend/src/components/GroupCard.tsx +143 -0
- package/frontend/src/components/MarketServerCard.tsx +153 -0
- package/frontend/src/components/MarketServerDetail.tsx +297 -0
- package/frontend/src/components/ProtectedRoute.tsx +27 -0
- package/frontend/src/components/ServerCard.tsx +230 -0
- package/frontend/src/components/ServerForm.tsx +276 -0
- package/frontend/src/components/icons/LucideIcons.tsx +14 -0
- package/frontend/src/components/layout/Content.tsx +17 -0
- package/frontend/src/components/layout/Header.tsx +61 -0
- package/frontend/src/components/layout/Sidebar.tsx +98 -0
- package/frontend/src/components/ui/Badge.tsx +33 -0
- package/frontend/src/components/ui/Button.tsx +0 -0
- package/frontend/src/components/ui/DeleteDialog.tsx +48 -0
- package/frontend/src/components/ui/Pagination.tsx +128 -0
- package/frontend/src/components/ui/Toast.tsx +96 -0
- package/frontend/src/components/ui/ToggleGroup.tsx +134 -0
- package/frontend/src/components/ui/ToolCard.tsx +38 -0
- package/frontend/src/contexts/AuthContext.tsx +159 -0
- package/frontend/src/contexts/ToastContext.tsx +60 -0
- package/frontend/src/hooks/useGroupData.ts +232 -0
- package/frontend/src/hooks/useMarketData.ts +410 -0
- package/frontend/src/hooks/useServerData.ts +306 -0
- package/frontend/src/hooks/useSettingsData.ts +131 -0
- package/frontend/src/i18n.ts +42 -0
- package/frontend/src/index.css +20 -0
- package/frontend/src/layouts/MainLayout.tsx +33 -0
- package/frontend/src/locales/en.json +214 -0
- package/frontend/src/locales/zh.json +214 -0
- package/frontend/src/main.tsx +12 -0
- package/frontend/src/pages/Dashboard.tsx +206 -0
- package/frontend/src/pages/GroupsPage.tsx +116 -0
- package/frontend/src/pages/LoginPage.tsx +104 -0
- package/frontend/src/pages/MarketPage.tsx +356 -0
- package/frontend/src/pages/ServersPage.tsx +144 -0
- package/frontend/src/pages/SettingsPage.tsx +149 -0
- package/frontend/src/services/authService.ts +141 -0
- package/frontend/src/types/index.ts +160 -0
- package/frontend/src/utils/cn.ts +10 -0
- package/frontend/tsconfig.json +31 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +26 -0
- package/googled76ca578b6543fbc.html +1 -0
- package/jest.config.js +10 -0
- package/mcp_settings.json +45 -0
- package/package.json +3 -18
- package/servers.json +74722 -0
- package/src/config/index.ts +46 -0
- package/src/controllers/authController.ts +179 -0
- package/src/controllers/groupController.ts +341 -0
- package/src/controllers/marketController.ts +154 -0
- package/src/controllers/serverController.ts +303 -0
- package/src/index.ts +17 -0
- package/src/middlewares/auth.ts +28 -0
- package/src/middlewares/index.ts +43 -0
- package/src/models/User.ts +103 -0
- package/src/routes/index.ts +96 -0
- package/src/server.ts +72 -0
- package/src/services/groupService.ts +232 -0
- package/src/services/marketService.ts +116 -0
- package/src/services/mcpService.ts +385 -0
- package/src/services/sseService.ts +119 -0
- package/src/types/index.ts +129 -0
- package/src/utils/migration.ts +52 -0
- package/tsconfig.json +17 -0
- package/bin/cli.js +0 -45
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Server } from '@/types'
|
|
4
|
+
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react'
|
|
5
|
+
import Badge from '@/components/ui/Badge'
|
|
6
|
+
import ToolCard from '@/components/ui/ToolCard'
|
|
7
|
+
import DeleteDialog from '@/components/ui/DeleteDialog'
|
|
8
|
+
import { useToast } from '@/contexts/ToastContext'
|
|
9
|
+
|
|
10
|
+
interface ServerCardProps {
|
|
11
|
+
server: Server
|
|
12
|
+
onRemove: (serverName: string) => void
|
|
13
|
+
onEdit: (server: Server) => void
|
|
14
|
+
onToggle?: (server: Server, enabled: boolean) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ServerCard = ({ server, onRemove, onEdit, onToggle }: ServerCardProps) => {
|
|
18
|
+
const { t } = useTranslation()
|
|
19
|
+
const { showToast } = useToast()
|
|
20
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
21
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
|
22
|
+
const [isToggling, setIsToggling] = useState(false)
|
|
23
|
+
const [showErrorPopover, setShowErrorPopover] = useState(false)
|
|
24
|
+
const [copied, setCopied] = useState(false)
|
|
25
|
+
const errorPopoverRef = useRef<HTMLDivElement>(null)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
29
|
+
if (errorPopoverRef.current && !errorPopoverRef.current.contains(event.target as Node)) {
|
|
30
|
+
setShowErrorPopover(false)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
35
|
+
return () => {
|
|
36
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
37
|
+
}
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const handleRemove = (e: React.MouseEvent) => {
|
|
41
|
+
e.stopPropagation()
|
|
42
|
+
setShowDeleteDialog(true)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const handleEdit = (e: React.MouseEvent) => {
|
|
46
|
+
e.stopPropagation()
|
|
47
|
+
onEdit(server)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleToggle = async (e: React.MouseEvent) => {
|
|
51
|
+
e.stopPropagation()
|
|
52
|
+
if (isToggling || !onToggle) return
|
|
53
|
+
|
|
54
|
+
setIsToggling(true)
|
|
55
|
+
try {
|
|
56
|
+
await onToggle(server, !(server.enabled !== false))
|
|
57
|
+
} finally {
|
|
58
|
+
setIsToggling(false)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleErrorIconClick = (e: React.MouseEvent) => {
|
|
63
|
+
e.stopPropagation()
|
|
64
|
+
setShowErrorPopover(!showErrorPopover)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const copyToClipboard = (e: React.MouseEvent) => {
|
|
68
|
+
e.stopPropagation()
|
|
69
|
+
if (!server.error) return
|
|
70
|
+
|
|
71
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
72
|
+
navigator.clipboard.writeText(server.error).then(() => {
|
|
73
|
+
setCopied(true)
|
|
74
|
+
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
|
|
75
|
+
setTimeout(() => setCopied(false), 2000)
|
|
76
|
+
})
|
|
77
|
+
} else {
|
|
78
|
+
// Fallback for HTTP or unsupported clipboard API
|
|
79
|
+
const textArea = document.createElement('textarea')
|
|
80
|
+
textArea.value = server.error
|
|
81
|
+
// Avoid scrolling to bottom
|
|
82
|
+
textArea.style.position = 'fixed'
|
|
83
|
+
textArea.style.left = '-9999px'
|
|
84
|
+
document.body.appendChild(textArea)
|
|
85
|
+
textArea.focus()
|
|
86
|
+
textArea.select()
|
|
87
|
+
try {
|
|
88
|
+
document.execCommand('copy')
|
|
89
|
+
setCopied(true)
|
|
90
|
+
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
|
|
91
|
+
setTimeout(() => setCopied(false), 2000)
|
|
92
|
+
} catch (err) {
|
|
93
|
+
showToast(t('common.copyFailed') || 'Copy failed', 'error')
|
|
94
|
+
console.error('Copy to clipboard failed:', err)
|
|
95
|
+
}
|
|
96
|
+
document.body.removeChild(textArea)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handleConfirmDelete = () => {
|
|
101
|
+
onRemove(server.name)
|
|
102
|
+
setShowDeleteDialog(false)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
<div className={`bg-white shadow rounded-lg p-6 mb-6 ${server.enabled === false ? 'opacity-60' : ''}`}>
|
|
108
|
+
<div
|
|
109
|
+
className="flex justify-between items-center cursor-pointer"
|
|
110
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
111
|
+
>
|
|
112
|
+
<div className="flex items-center space-x-3">
|
|
113
|
+
<h2 className={`text-xl font-semibold ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'}`}>{server.name}</h2>
|
|
114
|
+
<Badge status={server.status} />
|
|
115
|
+
|
|
116
|
+
{server.error && (
|
|
117
|
+
<div className="relative">
|
|
118
|
+
<div
|
|
119
|
+
className="cursor-pointer"
|
|
120
|
+
onClick={handleErrorIconClick}
|
|
121
|
+
aria-label={t('server.viewErrorDetails')}
|
|
122
|
+
>
|
|
123
|
+
<AlertCircle className="text-red-500 hover:text-red-600" size={18} />
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{showErrorPopover && (
|
|
127
|
+
<div
|
|
128
|
+
ref={errorPopoverRef}
|
|
129
|
+
className="absolute z-10 mt-2 bg-white border border-gray-200 rounded-md shadow-lg p-0 w-120"
|
|
130
|
+
style={{
|
|
131
|
+
left: '-231px',
|
|
132
|
+
top: '24px',
|
|
133
|
+
maxHeight: '300px',
|
|
134
|
+
overflowY: 'auto',
|
|
135
|
+
width: '480px',
|
|
136
|
+
transform: 'translateX(50%)'
|
|
137
|
+
}}
|
|
138
|
+
onClick={(e) => e.stopPropagation()}
|
|
139
|
+
>
|
|
140
|
+
<div className="flex justify-between items-center sticky top-0 bg-white py-2 px-4 border-b border-gray-200 z-20 shadow-sm">
|
|
141
|
+
<div className="flex items-center space-x-2">
|
|
142
|
+
<h4 className="text-sm font-medium text-red-600">{t('server.errorDetails')}</h4>
|
|
143
|
+
<button
|
|
144
|
+
onClick={copyToClipboard}
|
|
145
|
+
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
|
146
|
+
title={t('common.copy')}
|
|
147
|
+
>
|
|
148
|
+
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
<button
|
|
152
|
+
onClick={(e) => {
|
|
153
|
+
e.stopPropagation()
|
|
154
|
+
setShowErrorPopover(false)
|
|
155
|
+
}}
|
|
156
|
+
className="text-gray-400 hover:text-gray-600"
|
|
157
|
+
>
|
|
158
|
+
✕
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="p-4 pt-2">
|
|
162
|
+
<pre className="text-sm text-gray-700 break-words whitespace-pre-wrap">{server.error}</pre>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex space-x-2">
|
|
170
|
+
<button
|
|
171
|
+
onClick={handleEdit}
|
|
172
|
+
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm"
|
|
173
|
+
>
|
|
174
|
+
{t('server.edit')}
|
|
175
|
+
</button>
|
|
176
|
+
<div className="flex items-center">
|
|
177
|
+
<button
|
|
178
|
+
onClick={handleToggle}
|
|
179
|
+
className={`px-3 py-1 text-sm rounded transition-colors ${
|
|
180
|
+
isToggling
|
|
181
|
+
? 'bg-gray-200 text-gray-500'
|
|
182
|
+
: server.enabled !== false
|
|
183
|
+
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
|
184
|
+
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
|
185
|
+
}`}
|
|
186
|
+
disabled={isToggling}
|
|
187
|
+
>
|
|
188
|
+
{isToggling
|
|
189
|
+
? t('common.processing')
|
|
190
|
+
: server.enabled !== false
|
|
191
|
+
? t('server.disable')
|
|
192
|
+
: t('server.enable')
|
|
193
|
+
}
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
<button
|
|
197
|
+
onClick={handleRemove}
|
|
198
|
+
className="px-3 py-1 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm"
|
|
199
|
+
>
|
|
200
|
+
{t('server.delete')}
|
|
201
|
+
</button>
|
|
202
|
+
<button className="text-gray-400 hover:text-gray-600">
|
|
203
|
+
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{isExpanded && server.tools && (
|
|
209
|
+
<div className="mt-6">
|
|
210
|
+
<h3 className={`text-lg font-medium ${server.enabled === false ? 'text-gray-600' : 'text-gray-900'} mb-4`}>{t('server.tools')}</h3>
|
|
211
|
+
<div className="space-y-4">
|
|
212
|
+
{server.tools.map((tool, index) => (
|
|
213
|
+
<ToolCard key={index} tool={tool} />
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<DeleteDialog
|
|
221
|
+
isOpen={showDeleteDialog}
|
|
222
|
+
onClose={() => setShowDeleteDialog(false)}
|
|
223
|
+
onConfirm={handleConfirmDelete}
|
|
224
|
+
serverName={server.name}
|
|
225
|
+
/>
|
|
226
|
+
</>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export default ServerCard
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Server, EnvVar, ServerFormData } from '@/types'
|
|
4
|
+
|
|
5
|
+
interface ServerFormProps {
|
|
6
|
+
onSubmit: (payload: any) => void
|
|
7
|
+
onCancel: () => void
|
|
8
|
+
initialData?: Server | null
|
|
9
|
+
modalTitle: string
|
|
10
|
+
formError?: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formError = null }: ServerFormProps) => {
|
|
14
|
+
const { t } = useTranslation()
|
|
15
|
+
const [serverType, setServerType] = useState<'sse' | 'stdio'>(
|
|
16
|
+
initialData && initialData.config && initialData.config.url ? 'sse' : 'stdio',
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const [formData, setFormData] = useState<ServerFormData>({
|
|
20
|
+
name: (initialData && initialData.name) || '',
|
|
21
|
+
url: (initialData && initialData.config && initialData.config.url) || '',
|
|
22
|
+
command: (initialData && initialData.config && initialData.config.command) || '',
|
|
23
|
+
arguments:
|
|
24
|
+
initialData && initialData.config && initialData.config.args
|
|
25
|
+
? Array.isArray(initialData.config.args)
|
|
26
|
+
? initialData.config.args.join(' ')
|
|
27
|
+
: String(initialData.config.args)
|
|
28
|
+
: '',
|
|
29
|
+
args: (initialData && initialData.config && initialData.config.args) || [],
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const [envVars, setEnvVars] = useState<EnvVar[]>(
|
|
33
|
+
initialData && initialData.config && initialData.config.env
|
|
34
|
+
? Object.entries(initialData.config.env).map(([key, value]) => ({ key, value }))
|
|
35
|
+
: [],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const [error, setError] = useState<string | null>(null)
|
|
39
|
+
const isEdit = !!initialData
|
|
40
|
+
|
|
41
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
42
|
+
const { name, value } = e.target
|
|
43
|
+
setFormData({ ...formData, [name]: value })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Transform space-separated arguments string into array
|
|
47
|
+
const handleArgsChange = (value: string) => {
|
|
48
|
+
let args = value.split(' ').filter((arg) => arg.trim() !== '')
|
|
49
|
+
setFormData({ ...formData, arguments: value, args })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => {
|
|
53
|
+
const newEnvVars = [...envVars]
|
|
54
|
+
newEnvVars[index][field] = value
|
|
55
|
+
setEnvVars(newEnvVars)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const addEnvVar = () => {
|
|
59
|
+
setEnvVars([...envVars, { key: '', value: '' }])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const removeEnvVar = (index: number) => {
|
|
63
|
+
const newEnvVars = [...envVars]
|
|
64
|
+
newEnvVars.splice(index, 1)
|
|
65
|
+
setEnvVars(newEnvVars)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Submit handler for server configuration
|
|
69
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
70
|
+
e.preventDefault()
|
|
71
|
+
setError(null)
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const env: Record<string, string> = {}
|
|
75
|
+
envVars.forEach(({ key, value }) => {
|
|
76
|
+
if (key.trim()) {
|
|
77
|
+
env[key.trim()] = value
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const payload = {
|
|
82
|
+
name: formData.name,
|
|
83
|
+
config:
|
|
84
|
+
serverType === 'sse'
|
|
85
|
+
? { url: formData.url }
|
|
86
|
+
: {
|
|
87
|
+
command: formData.command,
|
|
88
|
+
args: formData.args,
|
|
89
|
+
env: Object.keys(env).length > 0 ? env : undefined,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onSubmit(payload)
|
|
94
|
+
} catch (err) {
|
|
95
|
+
setError(`Error: ${err instanceof Error ? err.message : String(err)}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="bg-white shadow rounded-lg p-6 w-full max-w-xl max-h-screen overflow-y-auto">
|
|
101
|
+
<div className="flex justify-between items-center mb-4">
|
|
102
|
+
<h2 className="text-xl font-semibold text-gray-900">{modalTitle}</h2>
|
|
103
|
+
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
|
|
104
|
+
✕
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{(error || formError) && (
|
|
109
|
+
<div className="bg-red-50 text-red-700 p-3 rounded mb-4">
|
|
110
|
+
{formError || error}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<form onSubmit={handleSubmit}>
|
|
115
|
+
<div className="mb-4">
|
|
116
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
|
117
|
+
{t('server.name')}
|
|
118
|
+
</label>
|
|
119
|
+
<input
|
|
120
|
+
type="text"
|
|
121
|
+
name="name"
|
|
122
|
+
id="name"
|
|
123
|
+
value={formData.name}
|
|
124
|
+
onChange={handleInputChange}
|
|
125
|
+
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
126
|
+
placeholder="e.g.: time-mcp"
|
|
127
|
+
required
|
|
128
|
+
disabled={isEdit}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="mb-4">
|
|
133
|
+
<label className="block text-gray-700 text-sm font-bold mb-2">{t('server.type')}</label>
|
|
134
|
+
<div className="flex space-x-4">
|
|
135
|
+
<div>
|
|
136
|
+
<input
|
|
137
|
+
type="radio"
|
|
138
|
+
id="command"
|
|
139
|
+
name="serverType"
|
|
140
|
+
value="command"
|
|
141
|
+
checked={serverType === 'stdio'}
|
|
142
|
+
onChange={() => setServerType('stdio')}
|
|
143
|
+
className="mr-1"
|
|
144
|
+
/>
|
|
145
|
+
<label htmlFor="command">stdio</label>
|
|
146
|
+
</div>
|
|
147
|
+
<div>
|
|
148
|
+
<input
|
|
149
|
+
type="radio"
|
|
150
|
+
id="url"
|
|
151
|
+
name="serverType"
|
|
152
|
+
value="url"
|
|
153
|
+
checked={serverType === 'sse'}
|
|
154
|
+
onChange={() => setServerType('sse')}
|
|
155
|
+
className="mr-1"
|
|
156
|
+
/>
|
|
157
|
+
<label htmlFor="url">sse</label>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{serverType === 'sse' ? (
|
|
163
|
+
<div className="mb-4">
|
|
164
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
|
|
165
|
+
{t('server.url')}
|
|
166
|
+
</label>
|
|
167
|
+
<input
|
|
168
|
+
type="url"
|
|
169
|
+
name="url"
|
|
170
|
+
id="url"
|
|
171
|
+
value={formData.url}
|
|
172
|
+
onChange={handleInputChange}
|
|
173
|
+
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
174
|
+
placeholder="e.g.: http://localhost:3000/sse"
|
|
175
|
+
required={serverType === 'sse'}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
) : (
|
|
179
|
+
<>
|
|
180
|
+
<div className="mb-4">
|
|
181
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="command">
|
|
182
|
+
{t('server.command')}
|
|
183
|
+
</label>
|
|
184
|
+
<input
|
|
185
|
+
type="text"
|
|
186
|
+
name="command"
|
|
187
|
+
id="command"
|
|
188
|
+
value={formData.command}
|
|
189
|
+
onChange={handleInputChange}
|
|
190
|
+
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
191
|
+
placeholder="e.g.: npx"
|
|
192
|
+
required={serverType === 'stdio'}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="mb-4">
|
|
196
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="arguments">
|
|
197
|
+
{t('server.arguments')}
|
|
198
|
+
</label>
|
|
199
|
+
<input
|
|
200
|
+
type="text"
|
|
201
|
+
name="arguments"
|
|
202
|
+
id="arguments"
|
|
203
|
+
value={formData.arguments}
|
|
204
|
+
onChange={(e) => handleArgsChange(e.target.value)}
|
|
205
|
+
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
206
|
+
placeholder="e.g.: -y time-mcp"
|
|
207
|
+
required={serverType === 'stdio'}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div className="mb-4">
|
|
212
|
+
<div className="flex justify-between items-center mb-2">
|
|
213
|
+
<label className="block text-gray-700 text-sm font-bold">
|
|
214
|
+
{t('server.envVars')}
|
|
215
|
+
</label>
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
onClick={addEnvVar}
|
|
219
|
+
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center"
|
|
220
|
+
>
|
|
221
|
+
+ {t('server.add')}
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
{envVars.map((envVar, index) => (
|
|
225
|
+
<div key={index} className="flex items-center mb-2">
|
|
226
|
+
<div className="flex items-center space-x-2 flex-grow">
|
|
227
|
+
<input
|
|
228
|
+
type="text"
|
|
229
|
+
value={envVar.key}
|
|
230
|
+
onChange={(e) => handleEnvVarChange(index, 'key', e.target.value)}
|
|
231
|
+
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2"
|
|
232
|
+
placeholder={t('server.key')}
|
|
233
|
+
/>
|
|
234
|
+
<span className="flex items-center">:</span>
|
|
235
|
+
<input
|
|
236
|
+
type="text"
|
|
237
|
+
value={envVar.value}
|
|
238
|
+
onChange={(e) => handleEnvVarChange(index, 'value', e.target.value)}
|
|
239
|
+
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2"
|
|
240
|
+
placeholder={t('server.value')}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={() => removeEnvVar(index)}
|
|
246
|
+
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2"
|
|
247
|
+
>
|
|
248
|
+
- {t('server.remove')}
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
</>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
<div className="flex justify-end mt-6">
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
onClick={onCancel}
|
|
260
|
+
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2"
|
|
261
|
+
>
|
|
262
|
+
{t('server.cancel')}
|
|
263
|
+
</button>
|
|
264
|
+
<button
|
|
265
|
+
type="submit"
|
|
266
|
+
className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
|
|
267
|
+
>
|
|
268
|
+
{isEdit ? t('server.save') : t('server.add')}
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
</form>
|
|
272
|
+
</div>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export default ServerForm
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ChevronDown, ChevronRight, Edit, Trash, Copy, Check } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
export { ChevronDown, ChevronRight, Edit, Trash, Copy, Check }
|
|
4
|
+
|
|
5
|
+
const LucideIcons = {
|
|
6
|
+
ChevronDown,
|
|
7
|
+
ChevronRight,
|
|
8
|
+
Edit,
|
|
9
|
+
Trash,
|
|
10
|
+
Copy,
|
|
11
|
+
Check
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default LucideIcons
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ContentProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const Content: React.FC<ContentProps> = ({ children }) => {
|
|
8
|
+
return (
|
|
9
|
+
<main className="flex-1 p-6 overflow-auto">
|
|
10
|
+
<div className="max-w-5xl mx-auto">
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default Content;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useNavigate } from 'react-router-dom';
|
|
4
|
+
import { useAuth } from '@/contexts/AuthContext';
|
|
5
|
+
|
|
6
|
+
interface HeaderProps {
|
|
7
|
+
onToggleSidebar: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const { auth, logout } = useAuth();
|
|
14
|
+
|
|
15
|
+
const handleLogout = () => {
|
|
16
|
+
logout();
|
|
17
|
+
navigate('/login');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<header className="bg-white shadow-sm z-10">
|
|
22
|
+
<div className="flex justify-between items-center px-4 py-3">
|
|
23
|
+
<div className="flex items-center">
|
|
24
|
+
{/* 侧边栏切换按钮 */}
|
|
25
|
+
<button
|
|
26
|
+
onClick={onToggleSidebar}
|
|
27
|
+
className="p-2 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-100 focus:outline-none"
|
|
28
|
+
aria-label={t('app.toggleSidebar')}
|
|
29
|
+
>
|
|
30
|
+
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
31
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
32
|
+
</svg>
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
{/* 应用标题 */}
|
|
36
|
+
<h1 className="ml-4 text-xl font-bold text-gray-900">{t('app.title')}</h1>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{/* 用户信息和操作 */}
|
|
40
|
+
<div className="flex items-center space-x-4">
|
|
41
|
+
{auth.user && (
|
|
42
|
+
<span className="text-sm text-gray-700">
|
|
43
|
+
{t('app.welcomeUser', { username: auth.user.username })}
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
<div className="flex space-x-2">
|
|
48
|
+
<button
|
|
49
|
+
onClick={handleLogout}
|
|
50
|
+
className="px-3 py-1.5 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm"
|
|
51
|
+
>
|
|
52
|
+
{t('app.logout')}
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</header>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default Header;
|