@samanhappy/mcphub 0.0.9 → 0.0.10

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 (99) 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/bin/mcphub.js +3 -0
  22. package/dist/index.js +0 -1
  23. package/dist/index.js.map +1 -1
  24. package/doc/intro.md +73 -0
  25. package/doc/intro2.md +232 -0
  26. package/entrypoint.sh +10 -0
  27. package/frontend/favicon.ico +0 -0
  28. package/frontend/index.html +13 -0
  29. package/frontend/postcss.config.js +6 -0
  30. package/frontend/src/App.tsx +44 -0
  31. package/frontend/src/components/AddGroupForm.tsx +132 -0
  32. package/frontend/src/components/AddServerForm.tsx +90 -0
  33. package/frontend/src/components/ChangePasswordForm.tsx +158 -0
  34. package/frontend/src/components/EditGroupForm.tsx +149 -0
  35. package/frontend/src/components/EditServerForm.tsx +76 -0
  36. package/frontend/src/components/GroupCard.tsx +143 -0
  37. package/frontend/src/components/MarketServerCard.tsx +153 -0
  38. package/frontend/src/components/MarketServerDetail.tsx +297 -0
  39. package/frontend/src/components/ProtectedRoute.tsx +27 -0
  40. package/frontend/src/components/ServerCard.tsx +230 -0
  41. package/frontend/src/components/ServerForm.tsx +276 -0
  42. package/frontend/src/components/icons/LucideIcons.tsx +14 -0
  43. package/frontend/src/components/layout/Content.tsx +17 -0
  44. package/frontend/src/components/layout/Header.tsx +61 -0
  45. package/frontend/src/components/layout/Sidebar.tsx +98 -0
  46. package/frontend/src/components/ui/Badge.tsx +33 -0
  47. package/frontend/src/components/ui/Button.tsx +0 -0
  48. package/frontend/src/components/ui/DeleteDialog.tsx +48 -0
  49. package/frontend/src/components/ui/Pagination.tsx +128 -0
  50. package/frontend/src/components/ui/Toast.tsx +96 -0
  51. package/frontend/src/components/ui/ToggleGroup.tsx +134 -0
  52. package/frontend/src/components/ui/ToolCard.tsx +38 -0
  53. package/frontend/src/contexts/AuthContext.tsx +159 -0
  54. package/frontend/src/contexts/ToastContext.tsx +60 -0
  55. package/frontend/src/hooks/useGroupData.ts +232 -0
  56. package/frontend/src/hooks/useMarketData.ts +410 -0
  57. package/frontend/src/hooks/useServerData.ts +306 -0
  58. package/frontend/src/hooks/useSettingsData.ts +131 -0
  59. package/frontend/src/i18n.ts +42 -0
  60. package/frontend/src/index.css +20 -0
  61. package/frontend/src/layouts/MainLayout.tsx +33 -0
  62. package/frontend/src/locales/en.json +214 -0
  63. package/frontend/src/locales/zh.json +214 -0
  64. package/frontend/src/main.tsx +12 -0
  65. package/frontend/src/pages/Dashboard.tsx +206 -0
  66. package/frontend/src/pages/GroupsPage.tsx +116 -0
  67. package/frontend/src/pages/LoginPage.tsx +104 -0
  68. package/frontend/src/pages/MarketPage.tsx +356 -0
  69. package/frontend/src/pages/ServersPage.tsx +144 -0
  70. package/frontend/src/pages/SettingsPage.tsx +149 -0
  71. package/frontend/src/services/authService.ts +141 -0
  72. package/frontend/src/types/index.ts +160 -0
  73. package/frontend/src/utils/cn.ts +10 -0
  74. package/frontend/tsconfig.json +31 -0
  75. package/frontend/tsconfig.node.json +10 -0
  76. package/frontend/vite.config.ts +26 -0
  77. package/googled76ca578b6543fbc.html +1 -0
  78. package/jest.config.js +10 -0
  79. package/mcp_settings.json +45 -0
  80. package/package.json +5 -8
  81. package/servers.json +74722 -0
  82. package/src/config/index.ts +46 -0
  83. package/src/controllers/authController.ts +179 -0
  84. package/src/controllers/groupController.ts +341 -0
  85. package/src/controllers/marketController.ts +154 -0
  86. package/src/controllers/serverController.ts +303 -0
  87. package/src/index.ts +17 -0
  88. package/src/middlewares/auth.ts +28 -0
  89. package/src/middlewares/index.ts +43 -0
  90. package/src/models/User.ts +103 -0
  91. package/src/routes/index.ts +96 -0
  92. package/src/server.ts +72 -0
  93. package/src/services/groupService.ts +232 -0
  94. package/src/services/marketService.ts +116 -0
  95. package/src/services/mcpService.ts +385 -0
  96. package/src/services/sseService.ts +119 -0
  97. package/src/types/index.ts +129 -0
  98. package/src/utils/migration.ts +52 -0
  99. package/tsconfig.json +17 -0
@@ -0,0 +1,385 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
5
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
+ import { ServerInfo, ServerConfig } from '../types/index.js';
7
+ import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js';
8
+ import config from '../config/index.js';
9
+ import { getGroup } from './sseService.js';
10
+ import { getServersInGroup } from './groupService.js';
11
+
12
+ let currentServer: Server;
13
+
14
+ export const initMcpServer = async (name: string, version: string): Promise<void> => {
15
+ currentServer = createMcpServer(name, version);
16
+ await registerAllTools(currentServer, true, true);
17
+ };
18
+
19
+ export const setMcpServer = (server: Server): void => {
20
+ currentServer = server;
21
+ };
22
+
23
+ export const getMcpServer = (): Server => {
24
+ return currentServer;
25
+ };
26
+
27
+ export const notifyToolChanged = async () => {
28
+ await registerAllTools(currentServer, true, false);
29
+ currentServer
30
+ .sendToolListChanged()
31
+ .catch((error) => {
32
+ console.warn('Failed to send tool list changed notification:', error.message);
33
+ })
34
+ .then(() => {
35
+ console.log('Tool list changed notification sent successfully');
36
+ });
37
+ };
38
+
39
+ // Store all server information
40
+ let serverInfos: ServerInfo[] = [];
41
+
42
+ // Initialize MCP server clients
43
+ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] => {
44
+ const settings = loadSettings();
45
+ const existingServerInfos = serverInfos;
46
+ serverInfos = [];
47
+
48
+ for (const [name, conf] of Object.entries(settings.mcpServers)) {
49
+ // Skip disabled servers
50
+ if (conf.enabled === false) {
51
+ console.log(`Skipping disabled server: ${name}`);
52
+ serverInfos.push({
53
+ name,
54
+ status: 'disconnected',
55
+ error: null,
56
+ tools: [],
57
+ createTime: Date.now(),
58
+ enabled: false,
59
+ });
60
+ continue;
61
+ }
62
+
63
+ // Check if server is already connected
64
+ const existingServer = existingServerInfos.find(
65
+ (s) => s.name === name && s.status === 'connected',
66
+ );
67
+ if (existingServer) {
68
+ serverInfos.push({
69
+ ...existingServer,
70
+ enabled: conf.enabled === undefined ? true : conf.enabled,
71
+ });
72
+ console.log(`Server '${name}' is already connected.`);
73
+ continue;
74
+ }
75
+
76
+ let transport;
77
+ if (conf.url) {
78
+ transport = new SSEClientTransport(new URL(conf.url));
79
+ } else if (conf.command && conf.args) {
80
+ const env: Record<string, string> = conf.env || {};
81
+ env['PATH'] = expandEnvVars(process.env.PATH as string) || '';
82
+ transport = new StdioClientTransport({
83
+ command: conf.command,
84
+ args: conf.args,
85
+ env: env,
86
+ });
87
+ } else {
88
+ console.warn(`Skipping server '${name}': missing required configuration`);
89
+ serverInfos.push({
90
+ name,
91
+ status: 'disconnected',
92
+ error: 'Missing required configuration',
93
+ tools: [],
94
+ createTime: Date.now(),
95
+ });
96
+ continue;
97
+ }
98
+
99
+ const client = new Client(
100
+ {
101
+ name: `mcp-client-${name}`,
102
+ version: '1.0.0',
103
+ },
104
+ {
105
+ capabilities: {
106
+ prompts: {},
107
+ resources: {},
108
+ tools: {},
109
+ },
110
+ },
111
+ );
112
+ const timeout = isInit ? Number(config.initTimeout) : Number(config.timeout);
113
+ client
114
+ .connect(transport, { timeout: timeout })
115
+ .then(() => {
116
+ console.log(`Successfully connected client for server: ${name}`);
117
+
118
+ client
119
+ .listTools({}, { timeout: timeout })
120
+ .then((tools) => {
121
+ console.log(`Successfully listed ${tools.tools.length} tools for server: ${name}`);
122
+ const serverInfo = getServerByName(name);
123
+ if (!serverInfo) {
124
+ console.warn(`Server info not found for server: ${name}`);
125
+ return;
126
+ }
127
+
128
+ serverInfo.tools = tools.tools.map((tool) => ({
129
+ name: tool.name,
130
+ description: tool.description || '',
131
+ inputSchema: tool.inputSchema || {},
132
+ }));
133
+ serverInfo.status = 'connected';
134
+ serverInfo.error = null;
135
+ })
136
+ .catch((error) => {
137
+ console.error(
138
+ `Failed to list tools for server ${name} by error: ${error} with stack: ${error.stack}`,
139
+ );
140
+ const serverInfo = getServerByName(name);
141
+ if (serverInfo) {
142
+ serverInfo.status = 'disconnected';
143
+ serverInfo.error = `Failed to list tools: ${error.stack} `;
144
+ }
145
+ });
146
+ })
147
+ .catch((error) => {
148
+ console.error(
149
+ `Failed to connect client for server ${name} by error: ${error} with stack: ${error.stack}`,
150
+ );
151
+ const serverInfo = getServerByName(name);
152
+ if (serverInfo) {
153
+ serverInfo.status = 'disconnected';
154
+ serverInfo.error = `Failed to connect: ${error.stack} `;
155
+ }
156
+ });
157
+ serverInfos.push({
158
+ name,
159
+ status: 'connecting',
160
+ error: null,
161
+ tools: [],
162
+ client,
163
+ transport,
164
+ createTime: Date.now(),
165
+ });
166
+ console.log(`Initialized client for server: ${name}`);
167
+ }
168
+
169
+ return serverInfos;
170
+ };
171
+
172
+ // Register all MCP tools
173
+ export const registerAllTools = async (
174
+ server: Server,
175
+ forceInit: boolean,
176
+ isInit: boolean,
177
+ ): Promise<void> => {
178
+ initializeClientsFromSettings(isInit);
179
+ };
180
+
181
+ // Get all server information
182
+ export const getServersInfo = (): Omit<ServerInfo, 'client' | 'transport'>[] => {
183
+ const settings = loadSettings();
184
+ const infos = serverInfos.map(({ name, status, tools, createTime, error }) => {
185
+ const serverConfig = settings.mcpServers[name];
186
+ const enabled = serverConfig ? serverConfig.enabled !== false : true;
187
+ return {
188
+ name,
189
+ status,
190
+ error,
191
+ tools,
192
+ createTime,
193
+ enabled,
194
+ };
195
+ });
196
+ infos.sort((a, b) => {
197
+ if (a.enabled === b.enabled) return 0;
198
+ return a.enabled ? -1 : 1;
199
+ });
200
+ return infos;
201
+ };
202
+
203
+ // Get server by name
204
+ const getServerByName = (name: string): ServerInfo | undefined => {
205
+ return serverInfos.find((serverInfo) => serverInfo.name === name);
206
+ };
207
+
208
+ // Get server by tool name
209
+ const getServerByTool = (toolName: string): ServerInfo | undefined => {
210
+ return serverInfos.find((serverInfo) => serverInfo.tools.some((tool) => tool.name === toolName));
211
+ };
212
+
213
+ // Add new server
214
+ export const addServer = async (
215
+ name: string,
216
+ config: ServerConfig,
217
+ ): Promise<{ success: boolean; message?: string }> => {
218
+ try {
219
+ const settings = loadSettings();
220
+ if (settings.mcpServers[name]) {
221
+ return { success: false, message: 'Server name already exists' };
222
+ }
223
+
224
+ settings.mcpServers[name] = config;
225
+ if (!saveSettings(settings)) {
226
+ return { success: false, message: 'Failed to save settings' };
227
+ }
228
+
229
+ registerAllTools(currentServer, false, false);
230
+ return { success: true, message: 'Server added successfully' };
231
+ } catch (error) {
232
+ console.error(`Failed to add server: ${name}`, error);
233
+ return { success: false, message: 'Failed to add server' };
234
+ }
235
+ };
236
+
237
+ // Remove server
238
+ export const removeServer = (name: string): { success: boolean; message?: string } => {
239
+ try {
240
+ const settings = loadSettings();
241
+ if (!settings.mcpServers[name]) {
242
+ return { success: false, message: 'Server not found' };
243
+ }
244
+
245
+ delete settings.mcpServers[name];
246
+
247
+ if (!saveSettings(settings)) {
248
+ return { success: false, message: 'Failed to save settings' };
249
+ }
250
+
251
+ serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
252
+ return { success: true, message: 'Server removed successfully' };
253
+ } catch (error) {
254
+ console.error(`Failed to remove server: ${name}`, error);
255
+ return { success: false, message: `Failed to remove server: ${error}` };
256
+ }
257
+ };
258
+
259
+ // Update existing server
260
+ export const updateMcpServer = async (
261
+ name: string,
262
+ config: ServerConfig,
263
+ ): Promise<{ success: boolean; message?: string }> => {
264
+ try {
265
+ const settings = loadSettings();
266
+ if (!settings.mcpServers[name]) {
267
+ return { success: false, message: 'Server not found' };
268
+ }
269
+
270
+ settings.mcpServers[name] = config;
271
+ if (!saveSettings(settings)) {
272
+ return { success: false, message: 'Failed to save settings' };
273
+ }
274
+
275
+ closeServer(name);
276
+
277
+ serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
278
+ return { success: true, message: 'Server updated successfully' };
279
+ } catch (error) {
280
+ console.error(`Failed to update server: ${name}`, error);
281
+ return { success: false, message: 'Failed to update server' };
282
+ }
283
+ };
284
+
285
+ // Close server client and transport
286
+ function closeServer(name: string) {
287
+ const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
288
+ if (serverInfo && serverInfo.client && serverInfo.transport) {
289
+ serverInfo.client.close();
290
+ serverInfo.transport.close();
291
+ console.log(`Closed client and transport for server: ${serverInfo.name}`);
292
+ // TODO kill process
293
+ }
294
+ }
295
+
296
+ // Toggle server enabled status
297
+ export const toggleServerStatus = async (
298
+ name: string,
299
+ enabled: boolean,
300
+ ): Promise<{ success: boolean; message?: string }> => {
301
+ try {
302
+ const settings = loadSettings();
303
+ if (!settings.mcpServers[name]) {
304
+ return { success: false, message: 'Server not found' };
305
+ }
306
+
307
+ // Update the enabled status in settings
308
+ settings.mcpServers[name].enabled = enabled;
309
+
310
+ if (!saveSettings(settings)) {
311
+ return { success: false, message: 'Failed to save settings' };
312
+ }
313
+
314
+ // If disabling, disconnect the server and remove from active servers
315
+ if (!enabled) {
316
+ closeServer(name);
317
+
318
+ // Update the server info to show as disconnected and disabled
319
+ const index = serverInfos.findIndex((s) => s.name === name);
320
+ if (index !== -1) {
321
+ serverInfos[index] = {
322
+ ...serverInfos[index],
323
+ status: 'disconnected',
324
+ enabled: false,
325
+ };
326
+ }
327
+ }
328
+
329
+ return { success: true, message: `Server ${enabled ? 'enabled' : 'disabled'} successfully` };
330
+ } catch (error) {
331
+ console.error(`Failed to toggle server status: ${name}`, error);
332
+ return { success: false, message: 'Failed to toggle server status' };
333
+ }
334
+ };
335
+
336
+ // Create McpServer instance
337
+ export const createMcpServer = (name: string, version: string): Server => {
338
+ const server = new Server({ name, version }, { capabilities: { tools: {} } });
339
+ server.setRequestHandler(ListToolsRequestSchema, async (_, extra) => {
340
+ const sessionId = extra.sessionId || '';
341
+ const group = getGroup(sessionId);
342
+ console.log(`Handling ListToolsRequest for group: ${group}`);
343
+ const allServerInfos = serverInfos.filter((serverInfo) => {
344
+ if (serverInfo.enabled === false) return false;
345
+ if (!group) return true;
346
+ const serversInGroup = getServersInGroup(group);
347
+ return serversInGroup.includes(serverInfo.name);
348
+ });
349
+
350
+ const allTools = [];
351
+ for (const serverInfo of allServerInfos) {
352
+ if (serverInfo.tools && serverInfo.tools.length > 0) {
353
+ allTools.push(...serverInfo.tools);
354
+ }
355
+ }
356
+
357
+ return {
358
+ tools: allTools,
359
+ };
360
+ });
361
+
362
+ server.setRequestHandler(CallToolRequestSchema, async (request, _) => {
363
+ console.log(`Handling CallToolRequest for tool: ${request.params.name}`);
364
+ try {
365
+ if (!request.params.arguments) {
366
+ throw new Error('Arguments are required');
367
+ }
368
+ const serverInfo = getServerByTool(request.params.name);
369
+ if (!serverInfo) {
370
+ throw new Error(`Server not found: ${request.params.name}`);
371
+ }
372
+ const client = serverInfo.client;
373
+ if (!client) {
374
+ throw new Error(`Client not found for server: ${request.params.name}`);
375
+ }
376
+ const result = await client.callTool(request.params);
377
+ console.log(`Tool call result: ${JSON.stringify(result)}`);
378
+ return result;
379
+ } catch (error) {
380
+ console.error(`Error handling CallToolRequest: ${error}`);
381
+ return { error: `Failed to call tool: ${error}` };
382
+ }
383
+ });
384
+ return server;
385
+ };
@@ -0,0 +1,119 @@
1
+ import { Request, Response } from 'express';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
4
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
5
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
7
+ import { getMcpServer } from './mcpService.js';
8
+ import { loadSettings } from '../config/index.js';
9
+
10
+ const transports: { [sessionId: string]: { transport: Transport; group: string } } = {};
11
+
12
+ export const getGroup = (sessionId: string): string => {
13
+ return transports[sessionId]?.group || '';
14
+ };
15
+
16
+ export const handleSseConnection = async (req: Request, res: Response): Promise<void> => {
17
+ const settings = loadSettings();
18
+ const routingConfig = settings.systemConfig?.routing || {
19
+ enableGlobalRoute: true,
20
+ enableGroupNameRoute: true,
21
+ };
22
+ const group = req.params.group;
23
+
24
+ // Check if this is a global route (no group) and if it's allowed
25
+ if (!group && !routingConfig.enableGlobalRoute) {
26
+ res.status(403).send('Global routes are disabled. Please specify a group ID.');
27
+ return;
28
+ }
29
+
30
+ const transport = new SSEServerTransport('/messages', res);
31
+ transports[transport.sessionId] = { transport, group: group };
32
+
33
+ res.on('close', () => {
34
+ delete transports[transport.sessionId];
35
+ console.log(`SSE connection closed: ${transport.sessionId}`);
36
+ });
37
+
38
+ console.log(
39
+ `New SSE connection established: ${transport.sessionId} with group: ${group || 'global'}`,
40
+ );
41
+ await getMcpServer().connect(transport);
42
+ };
43
+
44
+ export const handleSseMessage = async (req: Request, res: Response): Promise<void> => {
45
+ const sessionId = req.query.sessionId as string;
46
+ const { transport, group } = transports[sessionId];
47
+ req.params.group = group;
48
+ req.query.group = group;
49
+ console.log(`Received message for sessionId: ${sessionId} in group: ${group}`);
50
+ if (transport) {
51
+ await (transport as SSEServerTransport).handlePostMessage(req, res);
52
+ } else {
53
+ console.error(`No transport found for sessionId: ${sessionId}`);
54
+ res.status(400).send('No transport found for sessionId');
55
+ }
56
+ };
57
+
58
+ export const handleMcpPostRequest = async (req: Request, res: Response): Promise<void> => {
59
+ console.log('Handling MCP post request');
60
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
61
+ const group = req.params.group;
62
+ const settings = loadSettings();
63
+ const routingConfig = settings.systemConfig?.routing || {
64
+ enableGlobalRoute: true,
65
+ enableGroupNameRoute: true,
66
+ };
67
+ if (!group && !routingConfig.enableGlobalRoute) {
68
+ res.status(403).send('Global routes are disabled. Please specify a group ID.');
69
+ return;
70
+ }
71
+
72
+ let transport: StreamableHTTPServerTransport;
73
+ if (sessionId && transports[sessionId]) {
74
+ transport = transports[sessionId].transport as StreamableHTTPServerTransport;
75
+ } else if (!sessionId && isInitializeRequest(req.body)) {
76
+ transport = new StreamableHTTPServerTransport({
77
+ sessionIdGenerator: () => randomUUID(),
78
+ onsessioninitialized: (sessionId) => {
79
+ transports[sessionId] = { transport, group };
80
+ },
81
+ });
82
+
83
+ transport.onclose = () => {
84
+ if (transport.sessionId) {
85
+ delete transports[transport.sessionId];
86
+ }
87
+ };
88
+
89
+ await getMcpServer().connect(transport);
90
+ } else {
91
+ res.status(400).json({
92
+ jsonrpc: '2.0',
93
+ error: {
94
+ code: -32000,
95
+ message: 'Bad Request: No valid session ID provided',
96
+ },
97
+ id: null,
98
+ });
99
+ return;
100
+ }
101
+
102
+ await transport.handleRequest(req, res, req.body);
103
+ };
104
+
105
+ export const handleMcpOtherRequest = async (req: Request, res: Response) => {
106
+ console.log('Handling MCP other request');
107
+ const sessionId = req.headers['mcp-session-id'] as string | undefined;
108
+ if (!sessionId || !transports[sessionId]) {
109
+ res.status(400).send('Invalid or missing session ID');
110
+ return;
111
+ }
112
+
113
+ const { transport } = transports[sessionId];
114
+ await (transport as StreamableHTTPServerTransport).handleRequest(req, res);
115
+ };
116
+
117
+ export const getConnectionCount = (): number => {
118
+ return Object.keys(transports).length;
119
+ };
@@ -0,0 +1,129 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
3
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
4
+
5
+ // User interface
6
+ export interface IUser {
7
+ username: string;
8
+ password: string;
9
+ isAdmin?: boolean;
10
+ }
11
+
12
+ // Group interface for server grouping
13
+ export interface IGroup {
14
+ id: string; // Unique UUID for the group
15
+ name: string; // Display name of the group
16
+ description?: string; // Optional description of the group
17
+ servers: string[]; // Array of server names that belong to this group
18
+ }
19
+
20
+ // Market server types
21
+ export interface MarketServerRepository {
22
+ type: string;
23
+ url: string;
24
+ }
25
+
26
+ export interface MarketServerAuthor {
27
+ name: string;
28
+ }
29
+
30
+ export interface MarketServerInstallation {
31
+ type: string;
32
+ command: string;
33
+ args: string[];
34
+ env?: Record<string, string>;
35
+ }
36
+
37
+ export interface MarketServerArgument {
38
+ description: string;
39
+ required: boolean;
40
+ example: string;
41
+ }
42
+
43
+ export interface MarketServerExample {
44
+ title: string;
45
+ description: string;
46
+ prompt: string;
47
+ }
48
+
49
+ export interface MarketServerTool {
50
+ name: string;
51
+ description: string;
52
+ inputSchema: Record<string, any>;
53
+ }
54
+
55
+ export interface MarketServer {
56
+ name: string;
57
+ display_name: string;
58
+ description: string;
59
+ repository: MarketServerRepository;
60
+ homepage: string;
61
+ author: MarketServerAuthor;
62
+ license: string;
63
+ categories: string[];
64
+ tags: string[];
65
+ examples: MarketServerExample[];
66
+ installations: {
67
+ [key: string]: MarketServerInstallation;
68
+ };
69
+ arguments: Record<string, MarketServerArgument>;
70
+ tools: MarketServerTool[];
71
+ is_official?: boolean;
72
+ }
73
+
74
+ // Represents the settings for MCP servers
75
+ export interface McpSettings {
76
+ users?: IUser[]; // Array of user credentials and permissions
77
+ mcpServers: {
78
+ [key: string]: ServerConfig; // Key-value pairs of server names and their configurations
79
+ };
80
+ groups?: IGroup[]; // Array of server groups
81
+ systemConfig?: {
82
+ routing?: {
83
+ enableGlobalRoute?: boolean; // Controls whether the /sse endpoint without group is enabled
84
+ enableGroupNameRoute?: boolean; // Controls whether group routing by name is allowed
85
+ };
86
+ // Add other system configuration sections here in the future
87
+ };
88
+ }
89
+
90
+ // Configuration details for an individual server
91
+ export interface ServerConfig {
92
+ url?: string; // URL for SSE-based servers
93
+ command?: string; // Command to execute for stdio-based servers
94
+ args?: string[]; // Arguments for the command
95
+ env?: Record<string, string>; // Environment variables
96
+ enabled?: boolean; // Flag to enable/disable the server
97
+ }
98
+
99
+ // Information about a server's status and tools
100
+ export interface ServerInfo {
101
+ name: string; // Unique name of the server
102
+ status: 'connected' | 'connecting' | 'disconnected'; // Current connection status
103
+ error: string | null; // Error message if any
104
+ tools: ToolInfo[]; // List of tools available on the server
105
+ client?: Client; // Client instance for communication
106
+ transport?: SSEClientTransport | StdioClientTransport; // Transport mechanism used
107
+ createTime: number; // Timestamp of when the server was created
108
+ enabled?: boolean; // Flag to indicate if the server is enabled
109
+ }
110
+
111
+ // Details about a tool available on the server
112
+ export interface ToolInfo {
113
+ name: string; // Name of the tool
114
+ description: string; // Brief description of the tool
115
+ inputSchema: Record<string, unknown>; // Input schema for the tool
116
+ }
117
+
118
+ // Standardized API response structure
119
+ export interface ApiResponse<T = unknown> {
120
+ success: boolean; // Indicates if the operation was successful
121
+ message?: string; // Optional message providing additional details
122
+ data?: T; // Optional data payload
123
+ }
124
+
125
+ // Request payload for adding a new server
126
+ export interface AddServerRequest {
127
+ name: string; // Name of the server to add
128
+ config: ServerConfig; // Configuration details for the server
129
+ }
@@ -0,0 +1,52 @@
1
+ // filepath: /Users/sunmeng/code/github/mcphub/src/utils/migration.ts
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { loadSettings, saveSettings } from '../config/index.js';
5
+ import { IUser } from '../types/index.js';
6
+
7
+ /**
8
+ * Migrates user data from the old users.json file to mcp_settings.json
9
+ * This is a one-time migration to support the refactoring from separate
10
+ * users.json to integrated user data in mcp_settings.json
11
+ */
12
+ export const migrateUserData = (): void => {
13
+ const oldUsersFilePath = path.join(process.cwd(), 'data', 'users.json');
14
+
15
+ // Check if the old users file exists
16
+ if (fs.existsSync(oldUsersFilePath)) {
17
+ try {
18
+ // Read users from the old file
19
+ const usersData = fs.readFileSync(oldUsersFilePath, 'utf8');
20
+ const users = JSON.parse(usersData) as IUser[];
21
+
22
+ if (users && Array.isArray(users) && users.length > 0) {
23
+ console.log(`Migrating ${users.length} users from users.json to mcp_settings.json`);
24
+
25
+ // Load current settings
26
+ const settings = loadSettings();
27
+
28
+ // Merge users, giving priority to existing settings users
29
+ const existingUsernames = new Set((settings.users || []).map(u => u.username));
30
+ const newUsers = users.filter(u => !existingUsernames.has(u.username));
31
+
32
+ settings.users = [...(settings.users || []), ...newUsers];
33
+
34
+ // Save updated settings
35
+ if (saveSettings(settings)) {
36
+ console.log('User data migration completed successfully');
37
+
38
+ // Rename the old file as backup
39
+ const backupPath = `${oldUsersFilePath}.bak.${Date.now()}`;
40
+ fs.renameSync(oldUsersFilePath, backupPath);
41
+ console.log(`Renamed old users file to ${backupPath}`);
42
+ }
43
+ } else {
44
+ console.log('No users found in users.json, skipping migration');
45
+ }
46
+ } catch (error) {
47
+ console.error('Error during user data migration:', error);
48
+ }
49
+ } else {
50
+ console.log('users.json not found, no migration needed');
51
+ }
52
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "sourceMap": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "**/*.test.ts", "dist"]
17
+ }