@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.
Files changed (31) hide show
  1. package/README.md +16 -1
  2. package/bin/mcp-shark.js +179 -53
  3. package/mcp-server/lib/auditor/audit.js +12 -4
  4. package/mcp-server/lib/server/external/kv.js +17 -28
  5. package/mcp-server/lib/server/internal/handlers/prompts-get.js +14 -6
  6. package/mcp-server/lib/server/internal/handlers/prompts-list.js +9 -4
  7. package/mcp-server/lib/server/internal/handlers/resources-list.js +9 -4
  8. package/mcp-server/lib/server/internal/handlers/resources-read.js +13 -3
  9. package/mcp-server/lib/server/internal/handlers/tools-call.js +12 -8
  10. package/mcp-server/lib/server/internal/handlers/tools-list.js +4 -3
  11. package/mcp-server/lib/server/internal/run.js +1 -1
  12. package/mcp-server/lib/server/internal/server.js +10 -10
  13. package/mcp-server/lib/server/internal/session.js +14 -6
  14. package/package.json +4 -2
  15. package/ui/server/routes/composite.js +16 -0
  16. package/ui/server/routes/playground.js +45 -14
  17. package/ui/server/utils/config-update.js +136 -109
  18. package/ui/server.js +1 -0
  19. package/ui/src/components/GroupedByMcpView.jsx +0 -6
  20. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +46 -21
  21. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +10 -8
  22. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +60 -32
  23. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +10 -8
  24. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +46 -21
  25. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +10 -8
  26. package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +107 -0
  27. package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +65 -0
  28. package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +70 -0
  29. package/ui/src/components/McpPlayground/useMcpPlayground.js +68 -137
  30. package/ui/src/components/McpPlayground.jsx +100 -21
  31. 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 newSessionId = randomUUID();
26
+ const initialSessionId = randomUUID();
26
27
  const transport = new StreamableHTTPServerTransport({
27
- sessionIdGenerator: () => newSessionId,
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(newSessionId, transport);
33
+ storeTransportInSession(initialSessionId, transport);
33
34
  // Session creation will be logged as part of the request packet in audit.js
34
- return requestHandler(transport, req, res, auditLogger);
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.0",
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 MCP_SERVER_URL = 'http://localhost:9851/mcp';
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
- if (clientSessions.has(sessionId)) {
15
- return clientSessions.get(sessionId);
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(MCP_SERVER_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(sessionId, clientWrapper);
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
- if (sessionId && clientSessions.has(sessionId)) {
144
- const clientWrapper = clientSessions.get(sessionId);
145
- await clientWrapper.close();
146
- clientSessions.delete(sessionId);
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
- export function updateConfigFile(
60
- originalConfig,
61
- selectedServiceNames,
59
+ function shouldCreateBackup(
60
+ latestBackupPath,
62
61
  resolvedFilePath,
63
62
  content,
64
63
  mcpSharkLogs,
65
64
  broadcastLogUpdate
66
65
  ) {
67
- const hasMcpServers = originalConfig.mcpServers && typeof originalConfig.mcpServers === 'object';
68
- const hasServers = originalConfig.servers && typeof originalConfig.servers === 'object';
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
- let createdBackupPath = null;
110
- if (resolvedFilePath && fs.existsSync(resolvedFilePath)) {
111
- // Check if we need to create a backup by comparing with latest backup
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
- if (latestBackupPath && fs.existsSync(latestBackupPath)) {
74
+ // Normalize both contents for comparison (remove whitespace differences)
75
+ const normalizeContent = (str) => {
116
76
  try {
117
- const latestBackupContent = fs.readFileSync(latestBackupPath, 'utf-8');
118
- const currentContent = content || fs.readFileSync(resolvedFilePath, 'utf-8');
119
-
120
- // Normalize both contents for comparison (remove whitespace differences)
121
- const normalizeContent = (str) => {
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
- if (shouldCreateBackup) {
156
- // Create backup with new format: .mcp.json-mcpshark.<datetime>.json
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 backupLog = {
90
+ const skipLog = {
174
91
  timestamp,
175
92
  type: 'stdout',
176
- line: `[BACKUP] Created backup: ${createdBackupPath.replace(homedir(), '~')}`,
93
+ line: `[BACKUP] Skipped backup (no changes detected): ${resolvedFilePath.replace(homedir(), '~')}`,
177
94
  };
178
- mcpSharkLogs.push(backupLog);
95
+ mcpSharkLogs.push(skipLog);
179
96
  if (mcpSharkLogs.length > 10000) {
180
97
  mcpSharkLogs.shift();
181
98
  }
182
- broadcastLogUpdate(backupLog);
183
- } else {
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: '12px',
9
- borderBottom: `1px solid ${colors.borderLight}`,
8
+ padding: '16px 20px',
9
+ margin: '4px 8px',
10
+ borderRadius: '8px',
10
11
  cursor: 'pointer',
11
- background: isSelected ? colors.bgSecondary : colors.bgCard,
12
- transition: 'background 0.2s',
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
- fontSize: '12px',
39
- color: colors.textSecondary,
40
- marginTop: '4px',
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
- {prompt.description}
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
- prompts.map((prompt, idx) => (
35
- <PromptItem
36
- key={idx}
37
- prompt={prompt}
38
- isSelected={selectedPrompt?.name === prompt.name}
39
- onClick={() => onSelectPrompt(prompt)}
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
  );