@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.
Files changed (97) hide show
  1. package/.env.example +2 -0
  2. package/.eslintrc.json +25 -0
  3. package/.github/workflows/build.yml +51 -0
  4. package/.github/workflows/release.yml +19 -0
  5. package/.prettierrc +7 -0
  6. package/Dockerfile +51 -0
  7. package/assets/amap-edit.png +0 -0
  8. package/assets/amap-result.png +0 -0
  9. package/assets/cherry-mcp.png +0 -0
  10. package/assets/cursor-mcp.png +0 -0
  11. package/assets/cursor-query.png +0 -0
  12. package/assets/cursor-tools.png +0 -0
  13. package/assets/dashboard.png +0 -0
  14. package/assets/dashboard.zh.png +0 -0
  15. package/assets/group.png +0 -0
  16. package/assets/group.zh.png +0 -0
  17. package/assets/market.zh.png +0 -0
  18. package/assets/wegroup.jpg +0 -0
  19. package/assets/wegroup.png +0 -0
  20. package/assets/wexin.png +0 -0
  21. package/doc/intro.md +73 -0
  22. package/doc/intro2.md +232 -0
  23. package/entrypoint.sh +10 -0
  24. package/frontend/favicon.ico +0 -0
  25. package/frontend/index.html +13 -0
  26. package/frontend/postcss.config.js +6 -0
  27. package/frontend/src/App.tsx +44 -0
  28. package/frontend/src/components/AddGroupForm.tsx +132 -0
  29. package/frontend/src/components/AddServerForm.tsx +90 -0
  30. package/frontend/src/components/ChangePasswordForm.tsx +158 -0
  31. package/frontend/src/components/EditGroupForm.tsx +149 -0
  32. package/frontend/src/components/EditServerForm.tsx +76 -0
  33. package/frontend/src/components/GroupCard.tsx +143 -0
  34. package/frontend/src/components/MarketServerCard.tsx +153 -0
  35. package/frontend/src/components/MarketServerDetail.tsx +297 -0
  36. package/frontend/src/components/ProtectedRoute.tsx +27 -0
  37. package/frontend/src/components/ServerCard.tsx +230 -0
  38. package/frontend/src/components/ServerForm.tsx +276 -0
  39. package/frontend/src/components/icons/LucideIcons.tsx +14 -0
  40. package/frontend/src/components/layout/Content.tsx +17 -0
  41. package/frontend/src/components/layout/Header.tsx +61 -0
  42. package/frontend/src/components/layout/Sidebar.tsx +98 -0
  43. package/frontend/src/components/ui/Badge.tsx +33 -0
  44. package/frontend/src/components/ui/Button.tsx +0 -0
  45. package/frontend/src/components/ui/DeleteDialog.tsx +48 -0
  46. package/frontend/src/components/ui/Pagination.tsx +128 -0
  47. package/frontend/src/components/ui/Toast.tsx +96 -0
  48. package/frontend/src/components/ui/ToggleGroup.tsx +134 -0
  49. package/frontend/src/components/ui/ToolCard.tsx +38 -0
  50. package/frontend/src/contexts/AuthContext.tsx +159 -0
  51. package/frontend/src/contexts/ToastContext.tsx +60 -0
  52. package/frontend/src/hooks/useGroupData.ts +232 -0
  53. package/frontend/src/hooks/useMarketData.ts +410 -0
  54. package/frontend/src/hooks/useServerData.ts +306 -0
  55. package/frontend/src/hooks/useSettingsData.ts +131 -0
  56. package/frontend/src/i18n.ts +42 -0
  57. package/frontend/src/index.css +20 -0
  58. package/frontend/src/layouts/MainLayout.tsx +33 -0
  59. package/frontend/src/locales/en.json +214 -0
  60. package/frontend/src/locales/zh.json +214 -0
  61. package/frontend/src/main.tsx +12 -0
  62. package/frontend/src/pages/Dashboard.tsx +206 -0
  63. package/frontend/src/pages/GroupsPage.tsx +116 -0
  64. package/frontend/src/pages/LoginPage.tsx +104 -0
  65. package/frontend/src/pages/MarketPage.tsx +356 -0
  66. package/frontend/src/pages/ServersPage.tsx +144 -0
  67. package/frontend/src/pages/SettingsPage.tsx +149 -0
  68. package/frontend/src/services/authService.ts +141 -0
  69. package/frontend/src/types/index.ts +160 -0
  70. package/frontend/src/utils/cn.ts +10 -0
  71. package/frontend/tsconfig.json +31 -0
  72. package/frontend/tsconfig.node.json +10 -0
  73. package/frontend/vite.config.ts +26 -0
  74. package/googled76ca578b6543fbc.html +1 -0
  75. package/jest.config.js +10 -0
  76. package/mcp_settings.json +45 -0
  77. package/package.json +3 -18
  78. package/servers.json +74722 -0
  79. package/src/config/index.ts +46 -0
  80. package/src/controllers/authController.ts +179 -0
  81. package/src/controllers/groupController.ts +341 -0
  82. package/src/controllers/marketController.ts +154 -0
  83. package/src/controllers/serverController.ts +303 -0
  84. package/src/index.ts +17 -0
  85. package/src/middlewares/auth.ts +28 -0
  86. package/src/middlewares/index.ts +43 -0
  87. package/src/models/User.ts +103 -0
  88. package/src/routes/index.ts +96 -0
  89. package/src/server.ts +72 -0
  90. package/src/services/groupService.ts +232 -0
  91. package/src/services/marketService.ts +116 -0
  92. package/src/services/mcpService.ts +385 -0
  93. package/src/services/sseService.ts +119 -0
  94. package/src/types/index.ts +129 -0
  95. package/src/utils/migration.ts +52 -0
  96. package/tsconfig.json +17 -0
  97. package/bin/cli.js +0 -45
@@ -0,0 +1,410 @@
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
+ };
@@ -0,0 +1,306 @@
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
+ };