@samanhappy/mcphub 0.0.10 → 0.0.12
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 +8 -5
- 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/bin/mcphub.js +0 -3
- 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 -17
- 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,410 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { MarketServer, ApiResponse } from '@/types';
|
|
4
|
-
|
|
5
|
-
export const useMarketData = () => {
|
|
6
|
-
const { t } = useTranslation();
|
|
7
|
-
const [servers, setServers] = useState<MarketServer[]>([]);
|
|
8
|
-
const [allServers, setAllServers] = useState<MarketServer[]>([]);
|
|
9
|
-
const [categories, setCategories] = useState<string[]>([]);
|
|
10
|
-
const [tags, setTags] = useState<string[]>([]);
|
|
11
|
-
const [selectedCategory, setSelectedCategory] = useState<string>('');
|
|
12
|
-
const [selectedTag, setSelectedTag] = useState<string>('');
|
|
13
|
-
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
14
|
-
const [loading, setLoading] = useState(true);
|
|
15
|
-
const [error, setError] = useState<string | null>(null);
|
|
16
|
-
const [currentServer, setCurrentServer] = useState<MarketServer | null>(null);
|
|
17
|
-
const [installedServers, setInstalledServers] = useState<string[]>([]);
|
|
18
|
-
|
|
19
|
-
// Pagination states
|
|
20
|
-
const [currentPage, setCurrentPage] = useState(1);
|
|
21
|
-
const [serversPerPage, setServersPerPage] = useState(9);
|
|
22
|
-
const [totalPages, setTotalPages] = useState(1);
|
|
23
|
-
|
|
24
|
-
// Fetch all market servers
|
|
25
|
-
const fetchMarketServers = useCallback(async () => {
|
|
26
|
-
try {
|
|
27
|
-
setLoading(true);
|
|
28
|
-
const token = localStorage.getItem('mcphub_token');
|
|
29
|
-
const response = await fetch('/api/market/servers', {
|
|
30
|
-
headers: {
|
|
31
|
-
'x-auth-token': token || ''
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (!response.ok) {
|
|
36
|
-
throw new Error(`Status: ${response.status}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const data: ApiResponse<MarketServer[]> = await response.json();
|
|
40
|
-
|
|
41
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
42
|
-
setAllServers(data.data);
|
|
43
|
-
// Apply pagination to the fetched data
|
|
44
|
-
applyPagination(data.data, currentPage);
|
|
45
|
-
} else {
|
|
46
|
-
console.error('Invalid market servers data format:', data);
|
|
47
|
-
setError(t('market.fetchError'));
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
console.error('Error fetching market servers:', err);
|
|
51
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
52
|
-
} finally {
|
|
53
|
-
setLoading(false);
|
|
54
|
-
}
|
|
55
|
-
}, [t, currentPage]);
|
|
56
|
-
|
|
57
|
-
// Apply pagination to data
|
|
58
|
-
const applyPagination = useCallback((data: MarketServer[], page: number, itemsPerPage = serversPerPage) => {
|
|
59
|
-
const totalItems = data.length;
|
|
60
|
-
const calculatedTotalPages = Math.ceil(totalItems / itemsPerPage);
|
|
61
|
-
setTotalPages(calculatedTotalPages);
|
|
62
|
-
|
|
63
|
-
// Ensure current page is valid
|
|
64
|
-
const validPage = Math.max(1, Math.min(page, calculatedTotalPages));
|
|
65
|
-
if (validPage !== page) {
|
|
66
|
-
setCurrentPage(validPage);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const startIndex = (validPage - 1) * itemsPerPage;
|
|
70
|
-
const paginatedServers = data.slice(startIndex, startIndex + itemsPerPage);
|
|
71
|
-
setServers(paginatedServers);
|
|
72
|
-
}, [serversPerPage]);
|
|
73
|
-
|
|
74
|
-
// Change page
|
|
75
|
-
const changePage = useCallback((page: number) => {
|
|
76
|
-
setCurrentPage(page);
|
|
77
|
-
applyPagination(allServers, page, serversPerPage);
|
|
78
|
-
}, [allServers, applyPagination, serversPerPage]);
|
|
79
|
-
|
|
80
|
-
// Fetch all categories
|
|
81
|
-
const fetchCategories = useCallback(async () => {
|
|
82
|
-
try {
|
|
83
|
-
const token = localStorage.getItem('mcphub_token');
|
|
84
|
-
const response = await fetch('/api/market/categories', {
|
|
85
|
-
headers: {
|
|
86
|
-
'x-auth-token': token || ''
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (!response.ok) {
|
|
91
|
-
throw new Error(`Status: ${response.status}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const data: ApiResponse<string[]> = await response.json();
|
|
95
|
-
|
|
96
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
97
|
-
setCategories(data.data);
|
|
98
|
-
} else {
|
|
99
|
-
console.error('Invalid categories data format:', data);
|
|
100
|
-
}
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.error('Error fetching categories:', err);
|
|
103
|
-
}
|
|
104
|
-
}, []);
|
|
105
|
-
|
|
106
|
-
// Fetch all tags
|
|
107
|
-
const fetchTags = useCallback(async () => {
|
|
108
|
-
try {
|
|
109
|
-
const token = localStorage.getItem('mcphub_token');
|
|
110
|
-
const response = await fetch('/api/market/tags', {
|
|
111
|
-
headers: {
|
|
112
|
-
'x-auth-token': token || ''
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
throw new Error(`Status: ${response.status}`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const data: ApiResponse<string[]> = await response.json();
|
|
121
|
-
|
|
122
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
123
|
-
setTags(data.data);
|
|
124
|
-
} else {
|
|
125
|
-
console.error('Invalid tags data format:', data);
|
|
126
|
-
}
|
|
127
|
-
} catch (err) {
|
|
128
|
-
console.error('Error fetching tags:', err);
|
|
129
|
-
}
|
|
130
|
-
}, []);
|
|
131
|
-
|
|
132
|
-
// Fetch server by name
|
|
133
|
-
const fetchServerByName = useCallback(async (name: string) => {
|
|
134
|
-
try {
|
|
135
|
-
setLoading(true);
|
|
136
|
-
const token = localStorage.getItem('mcphub_token');
|
|
137
|
-
const response = await fetch(`/api/market/servers/${name}`, {
|
|
138
|
-
headers: {
|
|
139
|
-
'x-auth-token': token || ''
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
throw new Error(`Status: ${response.status}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const data: ApiResponse<MarketServer> = await response.json();
|
|
148
|
-
|
|
149
|
-
if (data && data.success && data.data) {
|
|
150
|
-
setCurrentServer(data.data);
|
|
151
|
-
return data.data;
|
|
152
|
-
} else {
|
|
153
|
-
console.error('Invalid server data format:', data);
|
|
154
|
-
setError(t('market.serverNotFound'));
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
} catch (err) {
|
|
158
|
-
console.error(`Error fetching server ${name}:`, err);
|
|
159
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
160
|
-
return null;
|
|
161
|
-
} finally {
|
|
162
|
-
setLoading(false);
|
|
163
|
-
}
|
|
164
|
-
}, [t]);
|
|
165
|
-
|
|
166
|
-
// Search servers by query
|
|
167
|
-
const searchServers = useCallback(async (query: string) => {
|
|
168
|
-
try {
|
|
169
|
-
setLoading(true);
|
|
170
|
-
setSearchQuery(query);
|
|
171
|
-
|
|
172
|
-
if (!query.trim()) {
|
|
173
|
-
// Fetch fresh data from server instead of just applying pagination
|
|
174
|
-
fetchMarketServers();
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const token = localStorage.getItem('mcphub_token');
|
|
179
|
-
const response = await fetch(`/api/market/servers/search?query=${encodeURIComponent(query)}`, {
|
|
180
|
-
headers: {
|
|
181
|
-
'x-auth-token': token || ''
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
if (!response.ok) {
|
|
186
|
-
throw new Error(`Status: ${response.status}`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const data: ApiResponse<MarketServer[]> = await response.json();
|
|
190
|
-
|
|
191
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
192
|
-
setAllServers(data.data);
|
|
193
|
-
setCurrentPage(1);
|
|
194
|
-
applyPagination(data.data, 1);
|
|
195
|
-
} else {
|
|
196
|
-
console.error('Invalid search results format:', data);
|
|
197
|
-
setError(t('market.searchError'));
|
|
198
|
-
}
|
|
199
|
-
} catch (err) {
|
|
200
|
-
console.error('Error searching servers:', err);
|
|
201
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
202
|
-
} finally {
|
|
203
|
-
setLoading(false);
|
|
204
|
-
}
|
|
205
|
-
}, [t, allServers, applyPagination, fetchMarketServers]);
|
|
206
|
-
|
|
207
|
-
// Filter servers by category
|
|
208
|
-
const filterByCategory = useCallback(async (category: string) => {
|
|
209
|
-
try {
|
|
210
|
-
setLoading(true);
|
|
211
|
-
setSelectedCategory(category);
|
|
212
|
-
setSelectedTag(''); // Reset tag filter when filtering by category
|
|
213
|
-
|
|
214
|
-
if (!category) {
|
|
215
|
-
fetchMarketServers();
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const token = localStorage.getItem('mcphub_token');
|
|
220
|
-
const response = await fetch(`/api/market/categories/${encodeURIComponent(category)}`, {
|
|
221
|
-
headers: {
|
|
222
|
-
'x-auth-token': token || ''
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
if (!response.ok) {
|
|
227
|
-
throw new Error(`Status: ${response.status}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const data: ApiResponse<MarketServer[]> = await response.json();
|
|
231
|
-
|
|
232
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
233
|
-
setAllServers(data.data);
|
|
234
|
-
setCurrentPage(1);
|
|
235
|
-
applyPagination(data.data, 1);
|
|
236
|
-
} else {
|
|
237
|
-
console.error('Invalid category filter results format:', data);
|
|
238
|
-
setError(t('market.filterError'));
|
|
239
|
-
}
|
|
240
|
-
} catch (err) {
|
|
241
|
-
console.error('Error filtering servers by category:', err);
|
|
242
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
243
|
-
} finally {
|
|
244
|
-
setLoading(false);
|
|
245
|
-
}
|
|
246
|
-
}, [t, fetchMarketServers, applyPagination]);
|
|
247
|
-
|
|
248
|
-
// Filter servers by tag
|
|
249
|
-
const filterByTag = useCallback(async (tag: string) => {
|
|
250
|
-
try {
|
|
251
|
-
setLoading(true);
|
|
252
|
-
setSelectedTag(tag);
|
|
253
|
-
setSelectedCategory(''); // Reset category filter when filtering by tag
|
|
254
|
-
|
|
255
|
-
if (!tag) {
|
|
256
|
-
fetchMarketServers();
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const token = localStorage.getItem('mcphub_token');
|
|
261
|
-
const response = await fetch(`/api/market/tags/${encodeURIComponent(tag)}`, {
|
|
262
|
-
headers: {
|
|
263
|
-
'x-auth-token': token || ''
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
if (!response.ok) {
|
|
268
|
-
throw new Error(`Status: ${response.status}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const data: ApiResponse<MarketServer[]> = await response.json();
|
|
272
|
-
|
|
273
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
274
|
-
setAllServers(data.data);
|
|
275
|
-
setCurrentPage(1);
|
|
276
|
-
applyPagination(data.data, 1);
|
|
277
|
-
} else {
|
|
278
|
-
console.error('Invalid tag filter results format:', data);
|
|
279
|
-
setError(t('market.tagFilterError'));
|
|
280
|
-
}
|
|
281
|
-
} catch (err) {
|
|
282
|
-
console.error('Error filtering servers by tag:', err);
|
|
283
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
284
|
-
} finally {
|
|
285
|
-
setLoading(false);
|
|
286
|
-
}
|
|
287
|
-
}, [t, fetchMarketServers, applyPagination]);
|
|
288
|
-
|
|
289
|
-
// Fetch installed servers
|
|
290
|
-
const fetchInstalledServers = useCallback(async () => {
|
|
291
|
-
try {
|
|
292
|
-
const token = localStorage.getItem('mcphub_token');
|
|
293
|
-
const response = await fetch('/api/servers', {
|
|
294
|
-
headers: {
|
|
295
|
-
'x-auth-token': token || ''
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
if (!response.ok) {
|
|
300
|
-
throw new Error(`Status: ${response.status}`);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const data = await response.json();
|
|
304
|
-
|
|
305
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
306
|
-
// Extract server names
|
|
307
|
-
const installedServerNames = data.data.map((server: any) => server.name);
|
|
308
|
-
setInstalledServers(installedServerNames);
|
|
309
|
-
}
|
|
310
|
-
} catch (err) {
|
|
311
|
-
console.error('Error fetching installed servers:', err);
|
|
312
|
-
}
|
|
313
|
-
}, []);
|
|
314
|
-
|
|
315
|
-
// Check if a server is already installed
|
|
316
|
-
const isServerInstalled = useCallback((serverName: string) => {
|
|
317
|
-
return installedServers.includes(serverName);
|
|
318
|
-
}, [installedServers]);
|
|
319
|
-
|
|
320
|
-
// Install server to the local environment
|
|
321
|
-
const installServer = useCallback(async (server: MarketServer) => {
|
|
322
|
-
try {
|
|
323
|
-
const installType = server.installations?.npm ? 'npm' : Object.keys(server.installations || {}).length > 0 ? Object.keys(server.installations)[0] : null;
|
|
324
|
-
|
|
325
|
-
if (!installType || !server.installations?.[installType]) {
|
|
326
|
-
setError(t('market.noInstallationMethod'));
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const installation = server.installations[installType];
|
|
331
|
-
|
|
332
|
-
// Prepare server configuration
|
|
333
|
-
const serverConfig = {
|
|
334
|
-
name: server.name,
|
|
335
|
-
config: {
|
|
336
|
-
command: installation.command,
|
|
337
|
-
args: installation.args,
|
|
338
|
-
env: installation.env || {}
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
// Call the createServer API
|
|
343
|
-
const token = localStorage.getItem('mcphub_token');
|
|
344
|
-
const response = await fetch('/api/servers', {
|
|
345
|
-
method: 'POST',
|
|
346
|
-
headers: {
|
|
347
|
-
'Content-Type': 'application/json',
|
|
348
|
-
'x-auth-token': token || ''
|
|
349
|
-
},
|
|
350
|
-
body: JSON.stringify(serverConfig),
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
if (!response.ok) {
|
|
354
|
-
const errorData = await response.json();
|
|
355
|
-
throw new Error(errorData.message || `Status: ${response.status}`);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Update installed servers list after successful installation
|
|
359
|
-
await fetchInstalledServers();
|
|
360
|
-
return true;
|
|
361
|
-
} catch (err) {
|
|
362
|
-
console.error('Error installing server:', err);
|
|
363
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
}, [t, fetchInstalledServers]);
|
|
367
|
-
|
|
368
|
-
// Change servers per page
|
|
369
|
-
const changeServersPerPage = useCallback((perPage: number) => {
|
|
370
|
-
setServersPerPage(perPage);
|
|
371
|
-
setCurrentPage(1);
|
|
372
|
-
applyPagination(allServers, 1, perPage);
|
|
373
|
-
}, [allServers, applyPagination]);
|
|
374
|
-
|
|
375
|
-
// Load initial data
|
|
376
|
-
useEffect(() => {
|
|
377
|
-
fetchMarketServers();
|
|
378
|
-
fetchCategories();
|
|
379
|
-
fetchTags();
|
|
380
|
-
fetchInstalledServers();
|
|
381
|
-
}, [fetchMarketServers, fetchCategories, fetchTags, fetchInstalledServers]);
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
servers,
|
|
385
|
-
allServers,
|
|
386
|
-
categories,
|
|
387
|
-
tags,
|
|
388
|
-
selectedCategory,
|
|
389
|
-
selectedTag,
|
|
390
|
-
searchQuery,
|
|
391
|
-
loading,
|
|
392
|
-
error,
|
|
393
|
-
setError,
|
|
394
|
-
currentServer,
|
|
395
|
-
fetchMarketServers,
|
|
396
|
-
fetchServerByName,
|
|
397
|
-
searchServers,
|
|
398
|
-
filterByCategory,
|
|
399
|
-
filterByTag,
|
|
400
|
-
installServer,
|
|
401
|
-
// Pagination properties and methods
|
|
402
|
-
currentPage,
|
|
403
|
-
totalPages,
|
|
404
|
-
serversPerPage,
|
|
405
|
-
changePage,
|
|
406
|
-
changeServersPerPage,
|
|
407
|
-
// Installed servers methods
|
|
408
|
-
isServerInstalled
|
|
409
|
-
};
|
|
410
|
-
};
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { Server, ApiResponse } from '@/types';
|
|
4
|
-
|
|
5
|
-
// Configuration options
|
|
6
|
-
const CONFIG = {
|
|
7
|
-
// Initialization phase configuration
|
|
8
|
-
startup: {
|
|
9
|
-
maxAttempts: 60, // Maximum number of attempts during initialization
|
|
10
|
-
pollingInterval: 3000 // Polling interval during initialization (3 seconds)
|
|
11
|
-
},
|
|
12
|
-
// Normal operation phase configuration
|
|
13
|
-
normal: {
|
|
14
|
-
pollingInterval: 10000 // Polling interval during normal operation (10 seconds)
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const useServerData = () => {
|
|
19
|
-
const { t } = useTranslation();
|
|
20
|
-
const [servers, setServers] = useState<Server[]>([]);
|
|
21
|
-
const [error, setError] = useState<string | null>(null);
|
|
22
|
-
const [refreshKey, setRefreshKey] = useState(0);
|
|
23
|
-
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
|
24
|
-
const [fetchAttempts, setFetchAttempts] = useState(0);
|
|
25
|
-
|
|
26
|
-
// Timer reference for polling
|
|
27
|
-
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
28
|
-
// Track current attempt count to avoid dependency cycles
|
|
29
|
-
const attemptsRef = useRef<number>(0);
|
|
30
|
-
|
|
31
|
-
// Clear the timer
|
|
32
|
-
const clearTimer = () => {
|
|
33
|
-
if (intervalRef.current) {
|
|
34
|
-
clearInterval(intervalRef.current);
|
|
35
|
-
intervalRef.current = null;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Start normal polling
|
|
40
|
-
const startNormalPolling = useCallback(() => {
|
|
41
|
-
// Ensure no other timers are running
|
|
42
|
-
clearTimer();
|
|
43
|
-
|
|
44
|
-
const fetchServers = async () => {
|
|
45
|
-
try {
|
|
46
|
-
const token = localStorage.getItem('mcphub_token');
|
|
47
|
-
const response = await fetch('/api/servers', {
|
|
48
|
-
headers: {
|
|
49
|
-
'x-auth-token': token || ''
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
const data = await response.json();
|
|
53
|
-
|
|
54
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
55
|
-
setServers(data.data);
|
|
56
|
-
} else if (data && Array.isArray(data)) {
|
|
57
|
-
setServers(data);
|
|
58
|
-
} else {
|
|
59
|
-
console.error('Invalid server data format:', data);
|
|
60
|
-
setServers([]);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Reset error state
|
|
64
|
-
setError(null);
|
|
65
|
-
} catch (err) {
|
|
66
|
-
console.error('Error fetching servers during normal polling:', err);
|
|
67
|
-
|
|
68
|
-
// Use friendly error message
|
|
69
|
-
if (!navigator.onLine) {
|
|
70
|
-
setError(t('errors.network'));
|
|
71
|
-
} else if (err instanceof TypeError && (
|
|
72
|
-
err.message.includes('NetworkError') ||
|
|
73
|
-
err.message.includes('Failed to fetch')
|
|
74
|
-
)) {
|
|
75
|
-
setError(t('errors.serverConnection'));
|
|
76
|
-
} else {
|
|
77
|
-
setError(t('errors.serverFetch'));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Execute immediately
|
|
83
|
-
fetchServers();
|
|
84
|
-
|
|
85
|
-
// Set up regular polling
|
|
86
|
-
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
|
|
87
|
-
}, [t]);
|
|
88
|
-
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
// Reset attempt count
|
|
91
|
-
if (refreshKey > 0) {
|
|
92
|
-
attemptsRef.current = 0;
|
|
93
|
-
setFetchAttempts(0);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Initialization phase request function
|
|
97
|
-
const fetchInitialData = async () => {
|
|
98
|
-
try {
|
|
99
|
-
const token = localStorage.getItem('mcphub_token');
|
|
100
|
-
const response = await fetch('/api/servers', {
|
|
101
|
-
headers: {
|
|
102
|
-
'x-auth-token': token || ''
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
const data = await response.json();
|
|
106
|
-
|
|
107
|
-
// Handle API response wrapper object, extract data field
|
|
108
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
109
|
-
setServers(data.data);
|
|
110
|
-
setIsInitialLoading(false);
|
|
111
|
-
// Initialization successful, start normal polling
|
|
112
|
-
startNormalPolling();
|
|
113
|
-
return true;
|
|
114
|
-
} else if (data && Array.isArray(data)) {
|
|
115
|
-
// Compatibility handling, if API directly returns array
|
|
116
|
-
setServers(data);
|
|
117
|
-
setIsInitialLoading(false);
|
|
118
|
-
// Initialization successful, start normal polling
|
|
119
|
-
startNormalPolling();
|
|
120
|
-
return true;
|
|
121
|
-
} else {
|
|
122
|
-
// If data format is not as expected, set to empty array
|
|
123
|
-
console.error('Invalid server data format:', data);
|
|
124
|
-
setServers([]);
|
|
125
|
-
setIsInitialLoading(false);
|
|
126
|
-
// Initialization successful but data is empty, start normal polling
|
|
127
|
-
startNormalPolling();
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
} catch (err) {
|
|
131
|
-
// Increment attempt count, use ref to avoid triggering effect rerun
|
|
132
|
-
attemptsRef.current += 1;
|
|
133
|
-
console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err);
|
|
134
|
-
|
|
135
|
-
// Update state for display
|
|
136
|
-
setFetchAttempts(attemptsRef.current);
|
|
137
|
-
|
|
138
|
-
// Set appropriate error message
|
|
139
|
-
if (!navigator.onLine) {
|
|
140
|
-
setError(t('errors.network'));
|
|
141
|
-
} else {
|
|
142
|
-
setError(t('errors.initialStartup'));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// If maximum attempt count is exceeded, give up initialization and switch to normal polling
|
|
146
|
-
if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
|
|
147
|
-
console.log('Maximum startup attempts reached, switching to normal polling');
|
|
148
|
-
setIsInitialLoading(false);
|
|
149
|
-
// Clear initialization polling
|
|
150
|
-
clearTimer();
|
|
151
|
-
// Switch to normal polling mode
|
|
152
|
-
startNormalPolling();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// On component mount, set appropriate polling based on current state
|
|
160
|
-
if (isInitialLoading) {
|
|
161
|
-
// Ensure no other timers are running
|
|
162
|
-
clearTimer();
|
|
163
|
-
|
|
164
|
-
// Execute initial request immediately
|
|
165
|
-
fetchInitialData();
|
|
166
|
-
|
|
167
|
-
// Set polling interval for initialization phase
|
|
168
|
-
intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval);
|
|
169
|
-
console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`);
|
|
170
|
-
} else {
|
|
171
|
-
// Initialization completed, start normal polling
|
|
172
|
-
startNormalPolling();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Cleanup function
|
|
176
|
-
return () => {
|
|
177
|
-
clearTimer();
|
|
178
|
-
};
|
|
179
|
-
}, [refreshKey, t, isInitialLoading, startNormalPolling]);
|
|
180
|
-
|
|
181
|
-
// Manually trigger refresh
|
|
182
|
-
const triggerRefresh = () => {
|
|
183
|
-
// Clear current timer
|
|
184
|
-
clearTimer();
|
|
185
|
-
|
|
186
|
-
// If in initialization phase, reset initialization state
|
|
187
|
-
if (isInitialLoading) {
|
|
188
|
-
setIsInitialLoading(true);
|
|
189
|
-
attemptsRef.current = 0;
|
|
190
|
-
setFetchAttempts(0);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Change in refreshKey will trigger useEffect to run again
|
|
194
|
-
setRefreshKey(prevKey => prevKey + 1);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
// Server related operations
|
|
198
|
-
const handleServerAdd = () => {
|
|
199
|
-
setRefreshKey(prevKey => prevKey + 1);
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const handleServerEdit = async (server: Server) => {
|
|
203
|
-
try {
|
|
204
|
-
// Fetch settings to get the full server config before editing
|
|
205
|
-
const token = localStorage.getItem('mcphub_token');
|
|
206
|
-
const response = await fetch(`/api/settings`, {
|
|
207
|
-
headers: {
|
|
208
|
-
'x-auth-token': token || ''
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> = await response.json();
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
settingsData &&
|
|
216
|
-
settingsData.success &&
|
|
217
|
-
settingsData.data &&
|
|
218
|
-
settingsData.data.mcpServers &&
|
|
219
|
-
settingsData.data.mcpServers[server.name]
|
|
220
|
-
) {
|
|
221
|
-
const serverConfig = settingsData.data.mcpServers[server.name];
|
|
222
|
-
return {
|
|
223
|
-
name: server.name,
|
|
224
|
-
status: server.status,
|
|
225
|
-
tools: server.tools || [],
|
|
226
|
-
config: serverConfig,
|
|
227
|
-
};
|
|
228
|
-
} else {
|
|
229
|
-
console.error('Failed to get server config from settings:', settingsData);
|
|
230
|
-
setError(t('server.invalidConfig', { serverName: server.name }));
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
} catch (err) {
|
|
234
|
-
console.error('Error fetching server settings:', err);
|
|
235
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const handleServerRemove = async (serverName: string) => {
|
|
241
|
-
try {
|
|
242
|
-
const token = localStorage.getItem('mcphub_token');
|
|
243
|
-
const response = await fetch(`/api/servers/${serverName}`, {
|
|
244
|
-
method: 'DELETE',
|
|
245
|
-
headers: {
|
|
246
|
-
'x-auth-token': token || ''
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
const result = await response.json();
|
|
250
|
-
|
|
251
|
-
if (!response.ok) {
|
|
252
|
-
setError(result.message || t('server.deleteError', { serverName }));
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
setRefreshKey(prevKey => prevKey + 1);
|
|
257
|
-
return true;
|
|
258
|
-
} catch (err) {
|
|
259
|
-
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const handleServerToggle = async (server: Server, enabled: boolean) => {
|
|
265
|
-
try {
|
|
266
|
-
const token = localStorage.getItem('mcphub_token');
|
|
267
|
-
const response = await fetch(`/api/servers/${server.name}/toggle`, {
|
|
268
|
-
method: 'POST',
|
|
269
|
-
headers: {
|
|
270
|
-
'Content-Type': 'application/json',
|
|
271
|
-
'x-auth-token': token || ''
|
|
272
|
-
},
|
|
273
|
-
body: JSON.stringify({ enabled }),
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
const result = await response.json();
|
|
277
|
-
|
|
278
|
-
if (!response.ok) {
|
|
279
|
-
console.error('Failed to toggle server:', result);
|
|
280
|
-
setError(t('server.toggleError', { serverName: server.name }));
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Update the UI immediately to reflect the change
|
|
285
|
-
setRefreshKey(prevKey => prevKey + 1);
|
|
286
|
-
return true;
|
|
287
|
-
} catch (err) {
|
|
288
|
-
console.error('Error toggling server:', err);
|
|
289
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
servers,
|
|
296
|
-
error,
|
|
297
|
-
setError,
|
|
298
|
-
isLoading: isInitialLoading,
|
|
299
|
-
fetchAttempts,
|
|
300
|
-
triggerRefresh,
|
|
301
|
-
handleServerAdd,
|
|
302
|
-
handleServerEdit,
|
|
303
|
-
handleServerRemove,
|
|
304
|
-
handleServerToggle
|
|
305
|
-
};
|
|
306
|
-
};
|