@samanhappy/mcphub 0.0.10 → 0.0.11

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