@mcp-shark/mcp-shark 1.4.0 → 1.4.2
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/README.md +16 -1
- package/bin/mcp-shark.js +179 -53
- package/mcp-server/lib/auditor/audit.js +12 -4
- package/mcp-server/lib/server/external/kv.js +17 -28
- package/mcp-server/lib/server/internal/handlers/prompts-get.js +14 -6
- package/mcp-server/lib/server/internal/handlers/prompts-list.js +9 -4
- package/mcp-server/lib/server/internal/handlers/resources-list.js +9 -4
- package/mcp-server/lib/server/internal/handlers/resources-read.js +13 -3
- package/mcp-server/lib/server/internal/handlers/tools-call.js +12 -8
- package/mcp-server/lib/server/internal/handlers/tools-list.js +4 -3
- package/mcp-server/lib/server/internal/run.js +1 -1
- package/mcp-server/lib/server/internal/server.js +10 -10
- package/mcp-server/lib/server/internal/session.js +14 -6
- package/package.json +4 -2
- package/ui/server/routes/composite.js +16 -0
- package/ui/server/routes/playground.js +45 -14
- package/ui/server/utils/config-update.js +136 -109
- package/ui/server.js +1 -0
- package/ui/src/components/GroupedByMcpView.jsx +0 -6
- package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +46 -21
- package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +10 -8
- package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +60 -32
- package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +10 -8
- package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +46 -21
- package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +10 -8
- package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +107 -0
- package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +65 -0
- package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +70 -0
- package/ui/src/components/McpPlayground/useMcpPlayground.js +68 -137
- package/ui/src/components/McpPlayground.jsx +100 -21
- package/ui/src/utils/requestUtils.js +5 -4
|
@@ -20,20 +20,28 @@ export async function withSession(
|
|
|
20
20
|
res,
|
|
21
21
|
auditLogger
|
|
22
22
|
) {
|
|
23
|
+
const requestedMcpServer = req.params[0];
|
|
23
24
|
const sessionId = getSessionFromRequest(req);
|
|
24
25
|
if (!sessionId) {
|
|
25
|
-
const
|
|
26
|
+
const initialSessionId = randomUUID();
|
|
26
27
|
const transport = new StreamableHTTPServerTransport({
|
|
27
|
-
sessionIdGenerator: () =>
|
|
28
|
+
sessionIdGenerator: () => initialSessionId,
|
|
28
29
|
enableJsonResponse: true,
|
|
29
30
|
});
|
|
30
|
-
const server = serverFactory();
|
|
31
|
+
const server = serverFactory(requestedMcpServer);
|
|
31
32
|
await server.connect(transport);
|
|
32
|
-
storeTransportInSession(
|
|
33
|
+
storeTransportInSession(initialSessionId, transport);
|
|
33
34
|
// Session creation will be logged as part of the request packet in audit.js
|
|
34
|
-
return requestHandler(
|
|
35
|
+
return requestHandler(
|
|
36
|
+
transport,
|
|
37
|
+
req,
|
|
38
|
+
res,
|
|
39
|
+
auditLogger,
|
|
40
|
+
requestedMcpServer,
|
|
41
|
+
initialSessionId
|
|
42
|
+
);
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
const transport = getTransportFromSession(sessionId);
|
|
38
|
-
return requestHandler(transport, req, res, auditLogger);
|
|
46
|
+
return requestHandler(transport, req, res, auditLogger, requestedMcpServer);
|
|
39
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-shark/mcp-shark",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Aggregate multiple Model Context Protocol (MCP) servers into a single unified interface with a powerful monitoring UI. Prov deep visibility into every request and response.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/mcp-shark.js",
|
|
@@ -85,7 +85,9 @@
|
|
|
85
85
|
"mcp-shark-common": "github:mcp-shark/mcp-shark-common",
|
|
86
86
|
"react": "^18.2.0",
|
|
87
87
|
"react-dom": "^18.2.0",
|
|
88
|
-
"ws": "^8.16.0"
|
|
88
|
+
"ws": "^8.16.0",
|
|
89
|
+
"open": "^11.0.0",
|
|
90
|
+
"commander": "^14.0.2"
|
|
89
91
|
},
|
|
90
92
|
"devDependencies": {
|
|
91
93
|
"@commitlint/cli": "^19.5.0",
|
|
@@ -240,5 +240,21 @@ export function createCompositeRoutes(
|
|
|
240
240
|
});
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
+
router.getServers = (req, res) => {
|
|
244
|
+
try {
|
|
245
|
+
const mcpsJsonPath = getMcpConfigPath();
|
|
246
|
+
if (!fs.existsSync(mcpsJsonPath)) {
|
|
247
|
+
return res.json({ servers: [] });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const configContent = fs.readFileSync(mcpsJsonPath, 'utf-8');
|
|
251
|
+
const config = JSON.parse(configContent);
|
|
252
|
+
const servers = config.servers ? Object.keys(config.servers) : [];
|
|
253
|
+
res.json({ servers });
|
|
254
|
+
} catch (error) {
|
|
255
|
+
res.status(500).json({ error: 'Failed to get servers', details: error.message });
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
243
259
|
return router;
|
|
244
260
|
}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
2
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const MCP_SERVER_BASE_URL = 'http://localhost:9851/mcp';
|
|
5
5
|
|
|
6
|
-
// Store client connections per session
|
|
6
|
+
// Store client connections per server and session
|
|
7
7
|
const clientSessions = new Map();
|
|
8
8
|
|
|
9
|
+
function getSessionKey(serverName, sessionId) {
|
|
10
|
+
return `${serverName}:${sessionId}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
export function createPlaygroundRoutes() {
|
|
10
14
|
const router = {};
|
|
11
15
|
|
|
12
|
-
// Get or create client for a session
|
|
13
|
-
async function getClient(sessionId) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
// Get or create client for a session and server
|
|
17
|
+
async function getClient(serverName, sessionId) {
|
|
18
|
+
const sessionKey = getSessionKey(serverName, sessionId);
|
|
19
|
+
if (clientSessions.has(sessionKey)) {
|
|
20
|
+
return clientSessions.get(sessionKey);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!serverName) {
|
|
24
|
+
throw new Error('Server name is required');
|
|
16
25
|
}
|
|
17
26
|
|
|
27
|
+
const mcpServerUrl = `${MCP_SERVER_BASE_URL}/${encodeURIComponent(serverName)}`;
|
|
28
|
+
|
|
18
29
|
const client = new Client(
|
|
19
30
|
{ name: 'mcp-shark-playground', version: '1.0.0' },
|
|
20
31
|
{
|
|
@@ -26,7 +37,7 @@ export function createPlaygroundRoutes() {
|
|
|
26
37
|
}
|
|
27
38
|
);
|
|
28
39
|
|
|
29
|
-
const transport = new StreamableHTTPClientTransport(new URL(
|
|
40
|
+
const transport = new StreamableHTTPClientTransport(new URL(mcpServerUrl));
|
|
30
41
|
await client.connect(transport);
|
|
31
42
|
|
|
32
43
|
const clientWrapper = {
|
|
@@ -38,13 +49,13 @@ export function createPlaygroundRoutes() {
|
|
|
38
49
|
},
|
|
39
50
|
};
|
|
40
51
|
|
|
41
|
-
clientSessions.set(
|
|
52
|
+
clientSessions.set(sessionKey, clientWrapper);
|
|
42
53
|
return clientWrapper;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
router.proxyRequest = async (req, res) => {
|
|
46
57
|
try {
|
|
47
|
-
const { method, params } = req.body;
|
|
58
|
+
const { method, params, serverName } = req.body;
|
|
48
59
|
|
|
49
60
|
if (!method) {
|
|
50
61
|
return res.status(400).json({
|
|
@@ -53,13 +64,20 @@ export function createPlaygroundRoutes() {
|
|
|
53
64
|
});
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
if (!serverName) {
|
|
68
|
+
return res.status(400).json({
|
|
69
|
+
error: 'Invalid request',
|
|
70
|
+
message: 'serverName field is required',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
// Get or create session ID
|
|
57
75
|
const sessionId =
|
|
58
76
|
req.headers['mcp-session-id'] ||
|
|
59
77
|
req.headers['x-mcp-session-id'] ||
|
|
60
78
|
`playground-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
61
79
|
|
|
62
|
-
const { client } = await getClient(sessionId);
|
|
80
|
+
const { client } = await getClient(serverName, sessionId);
|
|
63
81
|
|
|
64
82
|
let result;
|
|
65
83
|
switch (method) {
|
|
@@ -140,10 +158,23 @@ export function createPlaygroundRoutes() {
|
|
|
140
158
|
// Cleanup endpoint to close client connections
|
|
141
159
|
router.cleanup = async (req, res) => {
|
|
142
160
|
const sessionId = req.headers['mcp-session-id'] || req.headers['x-mcp-session-id'];
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
const { serverName } = req.body || {};
|
|
162
|
+
|
|
163
|
+
if (serverName && sessionId) {
|
|
164
|
+
const sessionKey = getSessionKey(serverName, sessionId);
|
|
165
|
+
if (clientSessions.has(sessionKey)) {
|
|
166
|
+
const clientWrapper = clientSessions.get(sessionKey);
|
|
167
|
+
await clientWrapper.close();
|
|
168
|
+
clientSessions.delete(sessionKey);
|
|
169
|
+
}
|
|
170
|
+
} else if (sessionId) {
|
|
171
|
+
// Cleanup all sessions for this sessionId across all servers
|
|
172
|
+
for (const [key, clientWrapper] of clientSessions.entries()) {
|
|
173
|
+
if (key.endsWith(`:${sessionId}`)) {
|
|
174
|
+
await clientWrapper.close();
|
|
175
|
+
clientSessions.delete(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
147
178
|
}
|
|
148
179
|
res.json({ success: true });
|
|
149
180
|
};
|
|
@@ -56,136 +56,137 @@ function findLatestBackup(filePath) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
selectedServiceNames,
|
|
59
|
+
function shouldCreateBackup(
|
|
60
|
+
latestBackupPath,
|
|
62
61
|
resolvedFilePath,
|
|
63
62
|
content,
|
|
64
63
|
mcpSharkLogs,
|
|
65
64
|
broadcastLogUpdate
|
|
66
65
|
) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const updatedConfig = { ...originalConfig };
|
|
71
|
-
|
|
72
|
-
if (hasMcpServers) {
|
|
73
|
-
const updatedMcpServers = {};
|
|
74
|
-
if (selectedServiceNames.size > 0) {
|
|
75
|
-
updatedMcpServers['mcp-shark-server'] = {
|
|
76
|
-
type: 'http',
|
|
77
|
-
url: 'http://localhost:9851/mcp',
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
Object.entries(originalConfig.mcpServers).forEach(([name, cfg]) => {
|
|
81
|
-
if (!selectedServiceNames.has(name)) {
|
|
82
|
-
updatedMcpServers[name] = cfg;
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
updatedConfig.mcpServers = updatedMcpServers;
|
|
86
|
-
} else if (hasServers) {
|
|
87
|
-
const updatedServers = {};
|
|
88
|
-
if (selectedServiceNames.size > 0) {
|
|
89
|
-
updatedServers['mcp-shark-server'] = {
|
|
90
|
-
type: 'http',
|
|
91
|
-
url: 'http://localhost:9851/mcp',
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
Object.entries(originalConfig.servers).forEach(([name, cfg]) => {
|
|
95
|
-
if (!selectedServiceNames.has(name)) {
|
|
96
|
-
updatedServers[name] = cfg;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
updatedConfig.servers = updatedServers;
|
|
100
|
-
} else {
|
|
101
|
-
updatedConfig.mcpServers = {
|
|
102
|
-
'mcp-shark-server': {
|
|
103
|
-
type: 'http',
|
|
104
|
-
url: 'http://localhost:9851/mcp',
|
|
105
|
-
},
|
|
106
|
-
};
|
|
66
|
+
if (!latestBackupPath || !fs.existsSync(latestBackupPath)) {
|
|
67
|
+
return true;
|
|
107
68
|
}
|
|
108
69
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const latestBackupPath = findLatestBackup(resolvedFilePath);
|
|
113
|
-
let shouldCreateBackup = true;
|
|
70
|
+
try {
|
|
71
|
+
const latestBackupContent = fs.readFileSync(latestBackupPath, 'utf-8');
|
|
72
|
+
const currentContent = content || fs.readFileSync(resolvedFilePath, 'utf-8');
|
|
114
73
|
|
|
115
|
-
|
|
74
|
+
// Normalize both contents for comparison (remove whitespace differences)
|
|
75
|
+
const normalizeContent = (str) => {
|
|
116
76
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
// Try to parse as JSON and re-stringify to normalize
|
|
124
|
-
return JSON.stringify(JSON.parse(str), null, 2);
|
|
125
|
-
} catch {
|
|
126
|
-
// If not valid JSON, just trim
|
|
127
|
-
return str.trim();
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const normalizedBackup = normalizeContent(latestBackupContent);
|
|
132
|
-
const normalizedCurrent = normalizeContent(currentContent);
|
|
133
|
-
|
|
134
|
-
if (normalizedBackup === normalizedCurrent) {
|
|
135
|
-
shouldCreateBackup = false;
|
|
136
|
-
const timestamp = new Date().toISOString();
|
|
137
|
-
const skipLog = {
|
|
138
|
-
timestamp,
|
|
139
|
-
type: 'stdout',
|
|
140
|
-
line: `[BACKUP] Skipped backup (no changes detected): ${resolvedFilePath.replace(homedir(), '~')}`,
|
|
141
|
-
};
|
|
142
|
-
mcpSharkLogs.push(skipLog);
|
|
143
|
-
if (mcpSharkLogs.length > 10000) {
|
|
144
|
-
mcpSharkLogs.shift();
|
|
145
|
-
}
|
|
146
|
-
broadcastLogUpdate(skipLog);
|
|
147
|
-
}
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.error('Error comparing with latest backup:', error);
|
|
150
|
-
// If comparison fails, create backup to be safe
|
|
151
|
-
shouldCreateBackup = true;
|
|
77
|
+
// Try to parse as JSON and re-stringify to normalize
|
|
78
|
+
return JSON.stringify(JSON.parse(str), null, 2);
|
|
79
|
+
} catch {
|
|
80
|
+
// If not valid JSON, just trim
|
|
81
|
+
return str.trim();
|
|
152
82
|
}
|
|
153
|
-
}
|
|
83
|
+
};
|
|
154
84
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const now = new Date();
|
|
158
|
-
// Format: YYYY-MM-DD_HH-MM-SS
|
|
159
|
-
const year = now.getFullYear();
|
|
160
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
161
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
162
|
-
const hours = String(now.getHours()).padStart(2, '0');
|
|
163
|
-
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
164
|
-
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
165
|
-
const datetimeStr = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
|
|
166
|
-
const dir = path.dirname(resolvedFilePath);
|
|
167
|
-
const basename = path.basename(resolvedFilePath);
|
|
168
|
-
createdBackupPath = path.join(dir, `.${basename}-mcpshark.${datetimeStr}.json`);
|
|
169
|
-
fs.copyFileSync(resolvedFilePath, createdBackupPath);
|
|
170
|
-
storeOriginalConfig(resolvedFilePath, content, createdBackupPath);
|
|
85
|
+
const normalizedBackup = normalizeContent(latestBackupContent);
|
|
86
|
+
const normalizedCurrent = normalizeContent(currentContent);
|
|
171
87
|
|
|
88
|
+
if (normalizedBackup === normalizedCurrent) {
|
|
172
89
|
const timestamp = new Date().toISOString();
|
|
173
|
-
const
|
|
90
|
+
const skipLog = {
|
|
174
91
|
timestamp,
|
|
175
92
|
type: 'stdout',
|
|
176
|
-
line: `[BACKUP]
|
|
93
|
+
line: `[BACKUP] Skipped backup (no changes detected): ${resolvedFilePath.replace(homedir(), '~')}`,
|
|
177
94
|
};
|
|
178
|
-
mcpSharkLogs.push(
|
|
95
|
+
mcpSharkLogs.push(skipLog);
|
|
179
96
|
if (mcpSharkLogs.length > 10000) {
|
|
180
97
|
mcpSharkLogs.shift();
|
|
181
98
|
}
|
|
182
|
-
broadcastLogUpdate(
|
|
183
|
-
|
|
184
|
-
// Still store the original config reference even if we didn't create a new backup
|
|
185
|
-
// Use the latest backup path if available
|
|
186
|
-
storeOriginalConfig(resolvedFilePath, content, latestBackupPath);
|
|
99
|
+
broadcastLogUpdate(skipLog);
|
|
100
|
+
return false;
|
|
187
101
|
}
|
|
102
|
+
return true;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error comparing with latest backup:', error);
|
|
105
|
+
// If comparison fails, create backup to be safe
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
188
109
|
|
|
110
|
+
function createBackup(resolvedFilePath, content, mcpSharkLogs, broadcastLogUpdate) {
|
|
111
|
+
// Create backup with new format: .mcp.json-mcpshark.<datetime>.json
|
|
112
|
+
const datetimeStr = formatDateTimeForBackup();
|
|
113
|
+
const dir = path.dirname(resolvedFilePath);
|
|
114
|
+
const basename = path.basename(resolvedFilePath);
|
|
115
|
+
const backupPath = path.join(dir, `.${basename}-mcpshark.${datetimeStr}.json`);
|
|
116
|
+
fs.copyFileSync(resolvedFilePath, backupPath);
|
|
117
|
+
storeOriginalConfig(resolvedFilePath, content, backupPath);
|
|
118
|
+
|
|
119
|
+
const timestamp = new Date().toISOString();
|
|
120
|
+
const backupLog = {
|
|
121
|
+
timestamp,
|
|
122
|
+
type: 'stdout',
|
|
123
|
+
line: `[BACKUP] Created backup: ${backupPath.replace(homedir(), '~')}`,
|
|
124
|
+
};
|
|
125
|
+
mcpSharkLogs.push(backupLog);
|
|
126
|
+
if (mcpSharkLogs.length > 10000) {
|
|
127
|
+
mcpSharkLogs.shift();
|
|
128
|
+
}
|
|
129
|
+
broadcastLogUpdate(backupLog);
|
|
130
|
+
return backupPath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function computeBackupPath(resolvedFilePath, content, mcpSharkLogs, broadcastLogUpdate) {
|
|
134
|
+
if (!resolvedFilePath || !fs.existsSync(resolvedFilePath)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if we need to create a backup by comparing with latest backup
|
|
139
|
+
const latestBackupPath = findLatestBackup(resolvedFilePath);
|
|
140
|
+
const needsBackup = shouldCreateBackup(
|
|
141
|
+
latestBackupPath,
|
|
142
|
+
resolvedFilePath,
|
|
143
|
+
content,
|
|
144
|
+
mcpSharkLogs,
|
|
145
|
+
broadcastLogUpdate
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (needsBackup) {
|
|
149
|
+
return createBackup(resolvedFilePath, content, mcpSharkLogs, broadcastLogUpdate);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Still store the original config reference even if we didn't create a new backup
|
|
153
|
+
// Use the latest backup path if available
|
|
154
|
+
storeOriginalConfig(resolvedFilePath, content, latestBackupPath);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function updateConfigFile(
|
|
159
|
+
originalConfig,
|
|
160
|
+
selectedServiceNames,
|
|
161
|
+
resolvedFilePath,
|
|
162
|
+
content,
|
|
163
|
+
mcpSharkLogs,
|
|
164
|
+
broadcastLogUpdate
|
|
165
|
+
) {
|
|
166
|
+
const [serverObject, serverType] = getServerObject(originalConfig);
|
|
167
|
+
const updatedConfig = { ...originalConfig };
|
|
168
|
+
|
|
169
|
+
if (serverObject) {
|
|
170
|
+
const updatedServers = {};
|
|
171
|
+
// Transform all original servers to HTTP URLs pointing to MCP shark server
|
|
172
|
+
// Each server gets its own endpoint to avoid tool name prefixing issues
|
|
173
|
+
Object.entries(serverObject).forEach(([name, cfg]) => {
|
|
174
|
+
updatedServers[name] = {
|
|
175
|
+
type: 'http',
|
|
176
|
+
url: `http://localhost:9851/mcp/${encodeURIComponent(name)}`,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
updatedConfig[serverType] = updatedServers;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const createdBackupPath = computeBackupPath(
|
|
183
|
+
resolvedFilePath,
|
|
184
|
+
content,
|
|
185
|
+
mcpSharkLogs,
|
|
186
|
+
broadcastLogUpdate
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (resolvedFilePath && fs.existsSync(resolvedFilePath)) {
|
|
189
190
|
fs.writeFileSync(resolvedFilePath, JSON.stringify(updatedConfig, null, 2));
|
|
190
191
|
console.log(`Updated config file: ${resolvedFilePath}`);
|
|
191
192
|
}
|
|
@@ -210,3 +211,29 @@ export function getSelectedServiceNames(originalConfig, selectedServices) {
|
|
|
210
211
|
|
|
211
212
|
return selectedServiceNames;
|
|
212
213
|
}
|
|
214
|
+
|
|
215
|
+
function getServerObject(originalConfig) {
|
|
216
|
+
const hasMcpServers = originalConfig.mcpServers && typeof originalConfig.mcpServers === 'object';
|
|
217
|
+
const hasServers = originalConfig.servers && typeof originalConfig.servers === 'object';
|
|
218
|
+
|
|
219
|
+
if (hasMcpServers) {
|
|
220
|
+
return [originalConfig.mcpServers, 'mcpServers'];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (hasServers) {
|
|
224
|
+
return [originalConfig.servers, 'servers'];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return [null, null];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function formatDateTimeForBackup() {
|
|
231
|
+
const now = new Date();
|
|
232
|
+
const year = now.getFullYear();
|
|
233
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
234
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
235
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
236
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
237
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
238
|
+
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
|
|
239
|
+
}
|
package/ui/server.js
CHANGED
|
@@ -120,6 +120,7 @@ export function createUIServer() {
|
|
|
120
120
|
compositeRoutes.stop(req, res, restoreConfig);
|
|
121
121
|
});
|
|
122
122
|
app.get('/api/composite/status', compositeRoutes.getStatus);
|
|
123
|
+
app.get('/api/composite/servers', compositeRoutes.getServers);
|
|
123
124
|
|
|
124
125
|
app.get('/api/help/state', helpRoutes.getState);
|
|
125
126
|
app.post('/api/help/dismiss', helpRoutes.dismiss);
|
|
@@ -15,12 +15,6 @@ function GroupedByMcpView({
|
|
|
15
15
|
onToggleSession,
|
|
16
16
|
onToggleCategory,
|
|
17
17
|
}) {
|
|
18
|
-
const getJsonRpcMethodFromPair = (pair) => {
|
|
19
|
-
const request = pair.request || pair.response;
|
|
20
|
-
if (!request) return null;
|
|
21
|
-
return getJsonRpcMethod(request);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
18
|
return (
|
|
25
19
|
<tbody>
|
|
26
20
|
{groupedData.map((sessionGroup) => {
|
|
@@ -5,44 +5,69 @@ export default function PromptItem({ prompt, isSelected, onClick }) {
|
|
|
5
5
|
<div
|
|
6
6
|
onClick={onClick}
|
|
7
7
|
style={{
|
|
8
|
-
padding: '
|
|
9
|
-
|
|
8
|
+
padding: '16px 20px',
|
|
9
|
+
margin: '4px 8px',
|
|
10
|
+
borderRadius: '8px',
|
|
10
11
|
cursor: 'pointer',
|
|
11
|
-
background: isSelected ? colors.
|
|
12
|
-
|
|
12
|
+
background: isSelected ? colors.bgSelected : colors.bgCard,
|
|
13
|
+
border: isSelected ? `2px solid ${colors.accentBlue}` : `1px solid ${colors.borderLight}`,
|
|
14
|
+
boxShadow: isSelected ? `0 2px 4px ${colors.shadowSm}` : 'none',
|
|
15
|
+
transition: 'all 0.2s ease',
|
|
16
|
+
position: 'relative',
|
|
13
17
|
}}
|
|
14
18
|
onMouseEnter={(e) => {
|
|
15
19
|
if (!isSelected) {
|
|
16
20
|
e.currentTarget.style.background = colors.bgHover;
|
|
21
|
+
e.currentTarget.style.borderColor = colors.borderMedium;
|
|
22
|
+
e.currentTarget.style.boxShadow = `0 2px 4px ${colors.shadowSm}`;
|
|
17
23
|
}
|
|
18
24
|
}}
|
|
19
25
|
onMouseLeave={(e) => {
|
|
20
26
|
if (!isSelected) {
|
|
21
27
|
e.currentTarget.style.background = colors.bgCard;
|
|
28
|
+
e.currentTarget.style.borderColor = colors.borderLight;
|
|
29
|
+
e.currentTarget.style.boxShadow = 'none';
|
|
22
30
|
}
|
|
23
31
|
}}
|
|
24
32
|
>
|
|
25
|
-
<div
|
|
26
|
-
style={{
|
|
27
|
-
fontWeight: '500',
|
|
28
|
-
fontSize: '13px',
|
|
29
|
-
color: colors.textPrimary,
|
|
30
|
-
marginBottom: '4px',
|
|
31
|
-
}}
|
|
32
|
-
>
|
|
33
|
-
{prompt.name}
|
|
34
|
-
</div>
|
|
35
|
-
{prompt.description && (
|
|
33
|
+
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
|
|
36
34
|
<div
|
|
37
35
|
style={{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
width: '6px',
|
|
37
|
+
height: '6px',
|
|
38
|
+
borderRadius: '50%',
|
|
39
|
+
background: isSelected ? colors.accentBlue : colors.textTertiary,
|
|
40
|
+
marginTop: '6px',
|
|
41
|
+
flexShrink: 0,
|
|
42
|
+
transition: 'background 0.2s',
|
|
41
43
|
}}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
/>
|
|
45
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
46
|
+
<div
|
|
47
|
+
style={{
|
|
48
|
+
fontWeight: isSelected ? '600' : '500',
|
|
49
|
+
fontSize: '14px',
|
|
50
|
+
color: colors.textPrimary,
|
|
51
|
+
marginBottom: prompt.description ? '6px' : '0',
|
|
52
|
+
lineHeight: '1.4',
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{prompt.name}
|
|
56
|
+
</div>
|
|
57
|
+
{prompt.description && (
|
|
58
|
+
<div
|
|
59
|
+
style={{
|
|
60
|
+
fontSize: '12px',
|
|
61
|
+
color: colors.textSecondary,
|
|
62
|
+
lineHeight: '1.5',
|
|
63
|
+
marginTop: '4px',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{prompt.description}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
44
69
|
</div>
|
|
45
|
-
|
|
70
|
+
</div>
|
|
46
71
|
</div>
|
|
47
72
|
);
|
|
48
73
|
}
|
|
@@ -31,14 +31,16 @@ export default function PromptsList({
|
|
|
31
31
|
) : prompts.length === 0 ? (
|
|
32
32
|
<EmptyState message="No prompts available." />
|
|
33
33
|
) : (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
<div style={{ padding: '8px 0' }}>
|
|
35
|
+
{prompts.map((prompt, idx) => (
|
|
36
|
+
<PromptItem
|
|
37
|
+
key={idx}
|
|
38
|
+
prompt={prompt}
|
|
39
|
+
isSelected={selectedPrompt?.name === prompt.name}
|
|
40
|
+
onClick={() => onSelectPrompt(prompt)}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
42
44
|
)}
|
|
43
45
|
</div>
|
|
44
46
|
);
|