@samanhappy/mcphub 0.0.7 → 0.0.8
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/package.json +4 -1
- package/.env.example +0 -2
- package/.eslintrc.json +0 -25
- package/.github/workflows/build.yml +0 -51
- package/.github/workflows/release.yml +0 -19
- package/.prettierrc +0 -7
- package/Dockerfile +0 -51
- 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 +0 -73
- package/doc/intro2.md +0 -232
- package/entrypoint.sh +0 -10
- package/frontend/favicon.ico +0 -0
- package/frontend/index.html +0 -13
- package/frontend/postcss.config.js +0 -6
- package/frontend/src/App.tsx +0 -44
- package/frontend/src/components/AddGroupForm.tsx +0 -132
- package/frontend/src/components/AddServerForm.tsx +0 -90
- package/frontend/src/components/ChangePasswordForm.tsx +0 -158
- package/frontend/src/components/EditGroupForm.tsx +0 -149
- package/frontend/src/components/EditServerForm.tsx +0 -76
- package/frontend/src/components/GroupCard.tsx +0 -143
- package/frontend/src/components/MarketServerCard.tsx +0 -153
- package/frontend/src/components/MarketServerDetail.tsx +0 -297
- package/frontend/src/components/ProtectedRoute.tsx +0 -27
- package/frontend/src/components/ServerCard.tsx +0 -230
- package/frontend/src/components/ServerForm.tsx +0 -276
- package/frontend/src/components/icons/LucideIcons.tsx +0 -14
- package/frontend/src/components/layout/Content.tsx +0 -17
- package/frontend/src/components/layout/Header.tsx +0 -61
- package/frontend/src/components/layout/Sidebar.tsx +0 -98
- package/frontend/src/components/ui/Badge.tsx +0 -33
- package/frontend/src/components/ui/Button.tsx +0 -0
- package/frontend/src/components/ui/DeleteDialog.tsx +0 -48
- package/frontend/src/components/ui/Pagination.tsx +0 -128
- package/frontend/src/components/ui/Toast.tsx +0 -96
- package/frontend/src/components/ui/ToggleGroup.tsx +0 -134
- package/frontend/src/components/ui/ToolCard.tsx +0 -38
- package/frontend/src/contexts/AuthContext.tsx +0 -159
- package/frontend/src/contexts/ToastContext.tsx +0 -60
- package/frontend/src/hooks/useGroupData.ts +0 -232
- package/frontend/src/hooks/useMarketData.ts +0 -410
- package/frontend/src/hooks/useServerData.ts +0 -306
- package/frontend/src/hooks/useSettingsData.ts +0 -131
- package/frontend/src/i18n.ts +0 -42
- package/frontend/src/index.css +0 -20
- package/frontend/src/layouts/MainLayout.tsx +0 -33
- package/frontend/src/locales/en.json +0 -214
- package/frontend/src/locales/zh.json +0 -214
- package/frontend/src/main.tsx +0 -12
- package/frontend/src/pages/Dashboard.tsx +0 -206
- package/frontend/src/pages/GroupsPage.tsx +0 -116
- package/frontend/src/pages/LoginPage.tsx +0 -104
- package/frontend/src/pages/MarketPage.tsx +0 -356
- package/frontend/src/pages/ServersPage.tsx +0 -144
- package/frontend/src/pages/SettingsPage.tsx +0 -149
- package/frontend/src/services/authService.ts +0 -141
- package/frontend/src/types/index.ts +0 -160
- package/frontend/src/utils/cn.ts +0 -10
- package/frontend/tsconfig.json +0 -31
- package/frontend/tsconfig.node.json +0 -10
- package/frontend/vite.config.ts +0 -26
- package/googled76ca578b6543fbc.html +0 -1
- package/jest.config.js +0 -10
- package/mcp_settings.json +0 -45
- package/servers.json +0 -74722
- package/src/config/index.ts +0 -46
- package/src/controllers/authController.ts +0 -179
- package/src/controllers/groupController.ts +0 -341
- package/src/controllers/marketController.ts +0 -154
- package/src/controllers/serverController.ts +0 -303
- package/src/index.ts +0 -18
- package/src/middlewares/auth.ts +0 -28
- package/src/middlewares/index.ts +0 -43
- package/src/models/User.ts +0 -103
- package/src/routes/index.ts +0 -96
- package/src/server.ts +0 -72
- package/src/services/groupService.ts +0 -232
- package/src/services/marketService.ts +0 -116
- package/src/services/mcpService.ts +0 -385
- package/src/services/sseService.ts +0 -119
- package/src/types/index.ts +0 -129
- package/src/utils/migration.ts +0 -52
- package/tsconfig.json +0 -17
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { useTranslation } from 'react-i18next'
|
|
3
|
-
import { Group, Server } from '@/types'
|
|
4
|
-
import { Edit, Trash, Copy, Check } from '@/components/icons/LucideIcons'
|
|
5
|
-
import DeleteDialog from '@/components/ui/DeleteDialog'
|
|
6
|
-
import { useToast } from '@/contexts/ToastContext'
|
|
7
|
-
|
|
8
|
-
interface GroupCardProps {
|
|
9
|
-
group: Group
|
|
10
|
-
servers: Server[]
|
|
11
|
-
onEdit: (group: Group) => void
|
|
12
|
-
onDelete: (groupId: string) => void
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const GroupCard = ({
|
|
16
|
-
group,
|
|
17
|
-
servers,
|
|
18
|
-
onEdit,
|
|
19
|
-
onDelete
|
|
20
|
-
}: GroupCardProps) => {
|
|
21
|
-
const { t } = useTranslation()
|
|
22
|
-
const { showToast } = useToast()
|
|
23
|
-
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
|
24
|
-
const [copied, setCopied] = useState(false)
|
|
25
|
-
|
|
26
|
-
const handleEdit = () => {
|
|
27
|
-
onEdit(group)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const handleDelete = () => {
|
|
31
|
-
setShowDeleteDialog(true)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const handleConfirmDelete = () => {
|
|
35
|
-
onDelete(group.id)
|
|
36
|
-
setShowDeleteDialog(false)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const copyToClipboard = () => {
|
|
40
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
41
|
-
navigator.clipboard.writeText(group.id).then(() => {
|
|
42
|
-
setCopied(true)
|
|
43
|
-
setTimeout(() => setCopied(false), 2000)
|
|
44
|
-
})
|
|
45
|
-
} else {
|
|
46
|
-
// Fallback for HTTP or unsupported clipboard API
|
|
47
|
-
const textArea = document.createElement('textarea')
|
|
48
|
-
textArea.value = group.id
|
|
49
|
-
// Avoid scrolling to bottom
|
|
50
|
-
textArea.style.position = 'fixed'
|
|
51
|
-
textArea.style.left = '-9999px'
|
|
52
|
-
document.body.appendChild(textArea)
|
|
53
|
-
textArea.focus()
|
|
54
|
-
textArea.select()
|
|
55
|
-
try {
|
|
56
|
-
document.execCommand('copy')
|
|
57
|
-
setCopied(true)
|
|
58
|
-
setTimeout(() => setCopied(false), 2000)
|
|
59
|
-
} catch (err) {
|
|
60
|
-
showToast(t('common.copyFailed') || 'Copy failed', 'error')
|
|
61
|
-
console.error('Copy to clipboard failed:', err)
|
|
62
|
-
}
|
|
63
|
-
document.body.removeChild(textArea)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Get servers that belong to this group
|
|
68
|
-
const groupServers = servers.filter(server => group.servers.includes(server.name))
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<div className="bg-white shadow rounded-lg p-6">
|
|
72
|
-
<div className="flex justify-between items-center mb-4">
|
|
73
|
-
<div>
|
|
74
|
-
<div className="flex items-center">
|
|
75
|
-
<h2 className="text-xl font-semibold text-gray-800">{group.name}</h2>
|
|
76
|
-
<div className="flex items-center ml-3">
|
|
77
|
-
<span className="text-xs text-gray-500 mr-1">{group.id}</span>
|
|
78
|
-
<button
|
|
79
|
-
onClick={copyToClipboard}
|
|
80
|
-
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
|
81
|
-
title={t('common.copy')}
|
|
82
|
-
>
|
|
83
|
-
{copied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
{group.description && (
|
|
88
|
-
<p className="text-gray-600 text-sm mt-1">{group.description}</p>
|
|
89
|
-
)}
|
|
90
|
-
</div>
|
|
91
|
-
<div className="flex items-center space-x-3">
|
|
92
|
-
<div className="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm">
|
|
93
|
-
{t('groups.serverCount', { count: group.servers.length })}
|
|
94
|
-
</div>
|
|
95
|
-
<button
|
|
96
|
-
onClick={handleEdit}
|
|
97
|
-
className="text-gray-500 hover:text-gray-700"
|
|
98
|
-
title={t('groups.edit')}
|
|
99
|
-
>
|
|
100
|
-
<Edit size={18} />
|
|
101
|
-
</button>
|
|
102
|
-
<button
|
|
103
|
-
onClick={handleDelete}
|
|
104
|
-
className="text-gray-500 hover:text-red-600"
|
|
105
|
-
title={t('groups.delete')}
|
|
106
|
-
>
|
|
107
|
-
<Trash size={18} />
|
|
108
|
-
</button>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div className="mt-4">
|
|
113
|
-
{groupServers.length === 0 ? (
|
|
114
|
-
<p className="text-gray-500 italic">{t('groups.noServers')}</p>
|
|
115
|
-
) : (
|
|
116
|
-
<div className="flex flex-wrap gap-2 mt-2">
|
|
117
|
-
{groupServers.map(server => (
|
|
118
|
-
<div
|
|
119
|
-
key={server.name}
|
|
120
|
-
className="inline-flex items-center px-3 py-1 bg-gray-50 rounded"
|
|
121
|
-
>
|
|
122
|
-
<span className="font-medium text-gray-700 text-sm">{server.name}</span>
|
|
123
|
-
<span className={`ml-2 inline-block h-2 w-2 rounded-full ${server.status === 'connected' ? 'bg-green-500' :
|
|
124
|
-
server.status === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'
|
|
125
|
-
}`}></span>
|
|
126
|
-
</div>
|
|
127
|
-
))}
|
|
128
|
-
</div>
|
|
129
|
-
)}
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<DeleteDialog
|
|
133
|
-
isOpen={showDeleteDialog}
|
|
134
|
-
onClose={() => setShowDeleteDialog(false)}
|
|
135
|
-
onConfirm={handleConfirmDelete}
|
|
136
|
-
serverName={group.name}
|
|
137
|
-
isGroup={true}
|
|
138
|
-
/>
|
|
139
|
-
</div>
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export default GroupCard
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { MarketServer } from '@/types';
|
|
4
|
-
|
|
5
|
-
interface MarketServerCardProps {
|
|
6
|
-
server: MarketServer;
|
|
7
|
-
onClick: (server: MarketServer) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const MarketServerCard: React.FC<MarketServerCardProps> = ({ server, onClick }) => {
|
|
11
|
-
const { t } = useTranslation();
|
|
12
|
-
|
|
13
|
-
// Intelligently calculate how many tags to display to ensure they fit in a single line
|
|
14
|
-
const getTagsToDisplay = () => {
|
|
15
|
-
if (!server.tags || server.tags.length === 0) {
|
|
16
|
-
return { tagsToShow: [], hasMore: false, moreCount: 0 };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Estimate available width in the card (in characters)
|
|
20
|
-
const estimatedAvailableWidth = 28; // Estimated number of characters that can fit in one line
|
|
21
|
-
|
|
22
|
-
// Calculate the character space needed for tags and plus sign (including # and spacing)
|
|
23
|
-
const calculateTagWidth = (tag: string) => tag.length + 3; // +3 for # and spacing
|
|
24
|
-
|
|
25
|
-
// Loop to determine the maximum number of tags that can be displayed
|
|
26
|
-
let totalWidth = 0;
|
|
27
|
-
let i = 0;
|
|
28
|
-
|
|
29
|
-
// First, sort tags by length to prioritize displaying shorter tags
|
|
30
|
-
const sortedTags = [...server.tags].sort((a, b) => a.length - b.length);
|
|
31
|
-
|
|
32
|
-
// Calculate how many tags can fit
|
|
33
|
-
for (i = 0; i < sortedTags.length; i++) {
|
|
34
|
-
const tagWidth = calculateTagWidth(sortedTags[i]);
|
|
35
|
-
|
|
36
|
-
// If this tag would make the total width exceed available width, stop adding
|
|
37
|
-
if (totalWidth + tagWidth > estimatedAvailableWidth) {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
totalWidth += tagWidth;
|
|
42
|
-
|
|
43
|
-
// If this is the last tag but there's still space, no need to show "more"
|
|
44
|
-
if (i === sortedTags.length - 1) {
|
|
45
|
-
return {
|
|
46
|
-
tagsToShow: sortedTags,
|
|
47
|
-
hasMore: false,
|
|
48
|
-
moreCount: 0
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// If there's not enough space to display any tags, show at least one
|
|
54
|
-
if (i === 0 && sortedTags.length > 0) {
|
|
55
|
-
i = 1;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Calculate space needed for the "more" tag
|
|
59
|
-
const moreCount = sortedTags.length - i;
|
|
60
|
-
const moreTagWidth = 3 + String(moreCount).length + t('market.moreTags').length;
|
|
61
|
-
|
|
62
|
-
// If there's enough remaining space to display the "more" tag
|
|
63
|
-
if (totalWidth + moreTagWidth <= estimatedAvailableWidth || i < 1) {
|
|
64
|
-
return {
|
|
65
|
-
tagsToShow: sortedTags.slice(0, i),
|
|
66
|
-
hasMore: true,
|
|
67
|
-
moreCount
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// If there's not enough space for even the "more" tag, reduce one tag to make room
|
|
72
|
-
return {
|
|
73
|
-
tagsToShow: sortedTags.slice(0, Math.max(1, i - 1)),
|
|
74
|
-
hasMore: true,
|
|
75
|
-
moreCount: moreCount + 1
|
|
76
|
-
};
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const { tagsToShow, hasMore, moreCount } = getTagsToDisplay();
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<div
|
|
83
|
-
className="bg-white rounded-lg shadow-md p-5 hover:shadow-lg transition-shadow cursor-pointer flex flex-col h-full"
|
|
84
|
-
onClick={() => onClick(server)}
|
|
85
|
-
>
|
|
86
|
-
<div className="flex justify-between items-start mb-3">
|
|
87
|
-
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1 mr-2">{server.display_name}</h3>
|
|
88
|
-
{server.is_official && (
|
|
89
|
-
<span className="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded flex-shrink-0">
|
|
90
|
-
{t('market.official')}
|
|
91
|
-
</span>
|
|
92
|
-
)}
|
|
93
|
-
</div>
|
|
94
|
-
<p className="text-gray-600 text-sm mb-4 line-clamp-2 min-h-[40px]">{server.description}</p>
|
|
95
|
-
|
|
96
|
-
{/* Categories */}
|
|
97
|
-
<div className="flex flex-wrap gap-1 mb-2 min-h-[28px]">
|
|
98
|
-
{server.categories?.length > 0 ? (
|
|
99
|
-
server.categories.map((category, index) => (
|
|
100
|
-
<span
|
|
101
|
-
key={index}
|
|
102
|
-
className="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded whitespace-nowrap"
|
|
103
|
-
>
|
|
104
|
-
{category}
|
|
105
|
-
</span>
|
|
106
|
-
))
|
|
107
|
-
) : (
|
|
108
|
-
<span className="text-xs text-gray-400 py-1">-</span>
|
|
109
|
-
)}
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
{/* Tags */}
|
|
113
|
-
<div className="relative mb-3 min-h-[28px] overflow-x-auto">
|
|
114
|
-
{server.tags?.length > 0 ? (
|
|
115
|
-
<div className="flex gap-1 items-center whitespace-nowrap">
|
|
116
|
-
{tagsToShow.map((tag, index) => (
|
|
117
|
-
<span
|
|
118
|
-
key={index}
|
|
119
|
-
className="bg-green-50 text-green-700 text-xs px-2 py-1 rounded flex-shrink-0"
|
|
120
|
-
>
|
|
121
|
-
#{tag}
|
|
122
|
-
</span>
|
|
123
|
-
))}
|
|
124
|
-
{hasMore && (
|
|
125
|
-
<span className="bg-gray-100 text-gray-600 text-xs px-1.5 py-1 rounded flex-shrink-0">
|
|
126
|
-
+{moreCount} {t('market.moreTags')}
|
|
127
|
-
</span>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
130
|
-
) : (
|
|
131
|
-
<span className="text-xs text-gray-400 py-1">-</span>
|
|
132
|
-
)}
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
<div className="flex justify-between items-center mt-auto pt-2 text-xs text-gray-500 border-t border-gray-100">
|
|
136
|
-
<div className="overflow-hidden">
|
|
137
|
-
<span className="whitespace-nowrap">{t('market.by')} </span>
|
|
138
|
-
<span className="font-medium whitespace-nowrap overflow-hidden text-ellipsis max-w-[120px] inline-block align-bottom">
|
|
139
|
-
{server.author?.name || t('market.unknown')}
|
|
140
|
-
</span>
|
|
141
|
-
</div>
|
|
142
|
-
<div className="flex items-center flex-shrink-0">
|
|
143
|
-
<svg className="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
144
|
-
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
|
|
145
|
-
</svg>
|
|
146
|
-
<span>{server.tools?.length || 0} {t('market.tools')}</span>
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
);
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
export default MarketServerCard;
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { MarketServer, MarketServerInstallation } from '@/types';
|
|
4
|
-
import ServerForm from './ServerForm';
|
|
5
|
-
|
|
6
|
-
interface MarketServerDetailProps {
|
|
7
|
-
server: MarketServer;
|
|
8
|
-
onBack: () => void;
|
|
9
|
-
onInstall: (server: MarketServer) => void;
|
|
10
|
-
installing?: boolean;
|
|
11
|
-
isInstalled?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const MarketServerDetail: React.FC<MarketServerDetailProps> = ({
|
|
15
|
-
server,
|
|
16
|
-
onBack,
|
|
17
|
-
onInstall,
|
|
18
|
-
installing = false,
|
|
19
|
-
isInstalled = false
|
|
20
|
-
}) => {
|
|
21
|
-
const { t } = useTranslation();
|
|
22
|
-
const [modalVisible, setModalVisible] = useState(false);
|
|
23
|
-
const [error, setError] = useState<string | null>(null);
|
|
24
|
-
|
|
25
|
-
// Helper function to determine button state
|
|
26
|
-
const getButtonProps = () => {
|
|
27
|
-
if (isInstalled) {
|
|
28
|
-
return {
|
|
29
|
-
className: "bg-green-600 cursor-default px-4 py-2 rounded text-sm font-medium text-white",
|
|
30
|
-
disabled: true,
|
|
31
|
-
text: t('market.installed')
|
|
32
|
-
};
|
|
33
|
-
} else if (installing) {
|
|
34
|
-
return {
|
|
35
|
-
className: "bg-gray-400 cursor-not-allowed px-4 py-2 rounded text-sm font-medium text-white",
|
|
36
|
-
disabled: true,
|
|
37
|
-
text: t('market.installing')
|
|
38
|
-
};
|
|
39
|
-
} else {
|
|
40
|
-
return {
|
|
41
|
-
className: "bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded text-sm font-medium text-white",
|
|
42
|
-
disabled: false,
|
|
43
|
-
text: t('market.install')
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const toggleModal = () => {
|
|
49
|
-
setModalVisible(!modalVisible);
|
|
50
|
-
setError(null); // Clear any previous errors when toggling modal
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const handleInstall = () => {
|
|
54
|
-
if (!isInstalled) {
|
|
55
|
-
toggleModal();
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Get the preferred installation configuration based on priority:
|
|
60
|
-
// npm > uvx > default
|
|
61
|
-
const getPreferredInstallation = (): MarketServerInstallation | undefined => {
|
|
62
|
-
if (!server.installations) {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (server.installations.npm) {
|
|
67
|
-
return server.installations.npm;
|
|
68
|
-
} else if (server.installations.uvx) {
|
|
69
|
-
return server.installations.uvx;
|
|
70
|
-
} else if (server.installations.default) {
|
|
71
|
-
return server.installations.default;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// If none of the preferred types are available, get the first available installation type
|
|
75
|
-
const installTypes = Object.keys(server.installations);
|
|
76
|
-
if (installTypes.length > 0) {
|
|
77
|
-
return server.installations[installTypes[0]];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return undefined;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const handleSubmit = async (payload: any) => {
|
|
84
|
-
try {
|
|
85
|
-
setError(null);
|
|
86
|
-
// Pass the server object to the parent component for installation
|
|
87
|
-
onInstall(server);
|
|
88
|
-
setModalVisible(false);
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error('Error installing server:', err);
|
|
91
|
-
setError(t('errors.serverInstall'));
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const buttonProps = getButtonProps();
|
|
96
|
-
const preferredInstallation = getPreferredInstallation();
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<div className="bg-white rounded-lg shadow-md p-6">
|
|
100
|
-
<div className="mb-4">
|
|
101
|
-
<button
|
|
102
|
-
onClick={onBack}
|
|
103
|
-
className="text-gray-600 hover:text-gray-900 flex items-center"
|
|
104
|
-
>
|
|
105
|
-
<svg className="h-5 w-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
106
|
-
<path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
|
|
107
|
-
</svg>
|
|
108
|
-
{t('market.backToList')}
|
|
109
|
-
</button>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div className="flex justify-between items-start mb-4">
|
|
113
|
-
<div>
|
|
114
|
-
<h2 className="text-2xl font-bold text-gray-900 flex items-center flex-wrap">
|
|
115
|
-
{server.display_name}
|
|
116
|
-
<span className="text-sm font-normal text-gray-500 ml-2">({server.name})</span>
|
|
117
|
-
<span className="text-sm font-normal text-gray-600 ml-4">
|
|
118
|
-
{t('market.author')}: {server.author.name} • {t('market.license')}: {server.license} •
|
|
119
|
-
<a
|
|
120
|
-
href={server.repository.url}
|
|
121
|
-
target="_blank"
|
|
122
|
-
rel="noopener noreferrer"
|
|
123
|
-
className="text-blue-600 hover:underline ml-1"
|
|
124
|
-
>
|
|
125
|
-
{t('market.repository')}
|
|
126
|
-
</a>
|
|
127
|
-
</span>
|
|
128
|
-
</h2>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<div className="flex items-center">
|
|
132
|
-
{server.is_official && (
|
|
133
|
-
<span className="bg-blue-100 text-blue-800 text-sm font-medium px-4 py-2 rounded mr-2 flex items-center">
|
|
134
|
-
{t('market.official')}
|
|
135
|
-
</span>
|
|
136
|
-
)}
|
|
137
|
-
<button
|
|
138
|
-
onClick={handleInstall}
|
|
139
|
-
disabled={buttonProps.disabled}
|
|
140
|
-
className={buttonProps.className}
|
|
141
|
-
>
|
|
142
|
-
{buttonProps.text}
|
|
143
|
-
</button>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
<p className="text-gray-700 mb-6">{server.description}</p>
|
|
148
|
-
|
|
149
|
-
<div className="mb-6">
|
|
150
|
-
<h3 className="text-lg font-semibold mb-3">{t('market.categories')} & {t('market.tags')}</h3>
|
|
151
|
-
<div className="flex flex-wrap gap-2">
|
|
152
|
-
{server.categories?.map((category, index) => (
|
|
153
|
-
<span key={`cat-${index}`} className="bg-gray-100 text-gray-800 px-3 py-1 rounded">
|
|
154
|
-
{category}
|
|
155
|
-
</span>
|
|
156
|
-
))}
|
|
157
|
-
{server.tags && server.tags.map((tag, index) => (
|
|
158
|
-
<span key={`tag-${index}`} className="bg-gray-100 text-green-700 px-2 py-1 rounded text-sm">
|
|
159
|
-
#{tag}
|
|
160
|
-
</span>
|
|
161
|
-
))}
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
{server.arguments && Object.keys(server.arguments).length > 0 && (
|
|
166
|
-
<div className="mb-6">
|
|
167
|
-
<h3 className="text-lg font-semibold mb-3">{t('market.arguments')}</h3>
|
|
168
|
-
<div className="overflow-x-auto">
|
|
169
|
-
<table className="min-w-full divide-y divide-gray-200">
|
|
170
|
-
<thead className="bg-gray-50">
|
|
171
|
-
<tr>
|
|
172
|
-
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
|
|
173
|
-
{t('market.argumentName')}
|
|
174
|
-
</th>
|
|
175
|
-
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
|
|
176
|
-
{t('market.description')}
|
|
177
|
-
</th>
|
|
178
|
-
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
|
|
179
|
-
{t('market.required')}
|
|
180
|
-
</th>
|
|
181
|
-
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider whitespace-nowrap">
|
|
182
|
-
{t('market.example')}
|
|
183
|
-
</th>
|
|
184
|
-
</tr>
|
|
185
|
-
</thead>
|
|
186
|
-
<tbody className="bg-white divide-y divide-gray-200">
|
|
187
|
-
{Object.entries(server.arguments).map(([name, arg], index) => (
|
|
188
|
-
<tr key={index}>
|
|
189
|
-
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
190
|
-
{name}
|
|
191
|
-
</td>
|
|
192
|
-
<td className="px-6 py-4 text-sm text-gray-500">
|
|
193
|
-
{arg.description}
|
|
194
|
-
</td>
|
|
195
|
-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
196
|
-
{arg.required ? (
|
|
197
|
-
<span className="text-green-600">✓</span>
|
|
198
|
-
) : (
|
|
199
|
-
<span className="text-red-600">✗</span>
|
|
200
|
-
)}
|
|
201
|
-
</td>
|
|
202
|
-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
203
|
-
<code className="bg-gray-100 px-2 py-1 rounded">{arg.example}</code>
|
|
204
|
-
</td>
|
|
205
|
-
</tr>
|
|
206
|
-
))}
|
|
207
|
-
</tbody>
|
|
208
|
-
</table>
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
)}
|
|
212
|
-
|
|
213
|
-
<div className="mb-6">
|
|
214
|
-
<h3 className="text-lg font-semibold mb-3">{t('market.tools')}</h3>
|
|
215
|
-
<div className="space-y-4">
|
|
216
|
-
{server.tools?.map((tool, index) => (
|
|
217
|
-
<div key={index} className="border border-gray-200 rounded p-4">
|
|
218
|
-
<h4 className="font-medium mb-2">
|
|
219
|
-
{tool.name}
|
|
220
|
-
<button
|
|
221
|
-
type="button"
|
|
222
|
-
onClick={() => {
|
|
223
|
-
// Toggle visibility of schema (simplified for this implementation)
|
|
224
|
-
const element = document.getElementById(`schema-${index}`);
|
|
225
|
-
if (element) {
|
|
226
|
-
element.classList.toggle('hidden');
|
|
227
|
-
}
|
|
228
|
-
}}
|
|
229
|
-
className="text-sm text-blue-600 hover:underline focus:outline-none ml-2"
|
|
230
|
-
>
|
|
231
|
-
{t('market.viewSchema')}
|
|
232
|
-
</button>
|
|
233
|
-
</h4>
|
|
234
|
-
<p className="text-gray-600 mb-2">{tool.description}</p>
|
|
235
|
-
<div className="mt-2">
|
|
236
|
-
<pre id={`schema-${index}`} className="hidden bg-gray-50 p-3 rounded text-sm overflow-auto mt-2">
|
|
237
|
-
{JSON.stringify(tool.inputSchema, null, 2)}
|
|
238
|
-
</pre>
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
))}
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
{server.examples && server.examples.length > 0 && (
|
|
246
|
-
<div className="mb-6">
|
|
247
|
-
<h3 className="text-lg font-semibold mb-3">{t('market.examples')}</h3>
|
|
248
|
-
<div className="space-y-4">
|
|
249
|
-
{server.examples.map((example, index) => (
|
|
250
|
-
<div key={index} className="border border-gray-200 rounded p-4">
|
|
251
|
-
<h4 className="font-medium mb-2">{example.title}</h4>
|
|
252
|
-
<p className="text-gray-600 mb-2">{example.description}</p>
|
|
253
|
-
<pre className="bg-gray-50 p-3 rounded text-sm overflow-auto">
|
|
254
|
-
{example.prompt}
|
|
255
|
-
</pre>
|
|
256
|
-
</div>
|
|
257
|
-
))}
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
)}
|
|
261
|
-
|
|
262
|
-
<div className="mt-6 flex justify-end">
|
|
263
|
-
<button
|
|
264
|
-
onClick={handleInstall}
|
|
265
|
-
disabled={buttonProps.disabled}
|
|
266
|
-
className={buttonProps.className}
|
|
267
|
-
>
|
|
268
|
-
{buttonProps.text}
|
|
269
|
-
</button>
|
|
270
|
-
</div>
|
|
271
|
-
|
|
272
|
-
{modalVisible && (
|
|
273
|
-
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
|
274
|
-
<ServerForm
|
|
275
|
-
onSubmit={handleSubmit}
|
|
276
|
-
onCancel={toggleModal}
|
|
277
|
-
modalTitle={t('market.installServer', { name: server.display_name })}
|
|
278
|
-
formError={error}
|
|
279
|
-
initialData={{
|
|
280
|
-
name: server.name,
|
|
281
|
-
status: 'disconnected',
|
|
282
|
-
config: preferredInstallation
|
|
283
|
-
? {
|
|
284
|
-
command: preferredInstallation.command || '',
|
|
285
|
-
args: preferredInstallation.args || [],
|
|
286
|
-
env: preferredInstallation.env || {}
|
|
287
|
-
}
|
|
288
|
-
: undefined
|
|
289
|
-
}}
|
|
290
|
-
/>
|
|
291
|
-
</div>
|
|
292
|
-
)}
|
|
293
|
-
</div>
|
|
294
|
-
);
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
export default MarketServerDetail;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Navigate, Outlet } from 'react-router-dom';
|
|
3
|
-
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { useAuth } from '../contexts/AuthContext';
|
|
5
|
-
|
|
6
|
-
interface ProtectedRouteProps {
|
|
7
|
-
redirectPath?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|
11
|
-
redirectPath = '/login'
|
|
12
|
-
}) => {
|
|
13
|
-
const { t } = useTranslation();
|
|
14
|
-
const { auth } = useAuth();
|
|
15
|
-
|
|
16
|
-
if (auth.loading) {
|
|
17
|
-
return <div className="flex items-center justify-center h-screen">{t('app.loading')}</div>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (!auth.isAuthenticated) {
|
|
21
|
-
return <Navigate to={redirectPath} replace />;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return <Outlet />;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default ProtectedRoute;
|