@mcp-shark/mcp-shark 1.4.1 → 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 (29) hide show
  1. package/mcp-server/lib/auditor/audit.js +12 -4
  2. package/mcp-server/lib/server/external/kv.js +17 -28
  3. package/mcp-server/lib/server/internal/handlers/prompts-get.js +14 -6
  4. package/mcp-server/lib/server/internal/handlers/prompts-list.js +9 -4
  5. package/mcp-server/lib/server/internal/handlers/resources-list.js +9 -4
  6. package/mcp-server/lib/server/internal/handlers/resources-read.js +13 -3
  7. package/mcp-server/lib/server/internal/handlers/tools-call.js +12 -8
  8. package/mcp-server/lib/server/internal/handlers/tools-list.js +4 -3
  9. package/mcp-server/lib/server/internal/run.js +1 -1
  10. package/mcp-server/lib/server/internal/server.js +10 -10
  11. package/mcp-server/lib/server/internal/session.js +14 -6
  12. package/package.json +1 -1
  13. package/ui/server/routes/composite.js +16 -0
  14. package/ui/server/routes/playground.js +45 -14
  15. package/ui/server/utils/config-update.js +136 -109
  16. package/ui/server.js +1 -0
  17. package/ui/src/components/GroupedByMcpView.jsx +0 -6
  18. package/ui/src/components/McpPlayground/PromptsSection/PromptItem.jsx +46 -21
  19. package/ui/src/components/McpPlayground/PromptsSection/PromptsList.jsx +10 -8
  20. package/ui/src/components/McpPlayground/ResourcesSection/ResourceItem.jsx +60 -32
  21. package/ui/src/components/McpPlayground/ResourcesSection/ResourcesList.jsx +10 -8
  22. package/ui/src/components/McpPlayground/ToolsSection/ToolItem.jsx +46 -21
  23. package/ui/src/components/McpPlayground/ToolsSection/ToolsList.jsx +10 -8
  24. package/ui/src/components/McpPlayground/hooks/useMcpDataLoader.js +107 -0
  25. package/ui/src/components/McpPlayground/hooks/useMcpRequest.js +65 -0
  26. package/ui/src/components/McpPlayground/hooks/useMcpServerStatus.js +70 -0
  27. package/ui/src/components/McpPlayground/useMcpPlayground.js +68 -137
  28. package/ui/src/components/McpPlayground.jsx +100 -21
  29. package/ui/src/utils/requestUtils.js +5 -4
@@ -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
  );
@@ -5,55 +5,83 @@ export default function ResourceItem({ resource, 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
- {resource.uri}
34
- </div>
35
- {resource.name && (
36
- <div
37
- style={{
38
- fontSize: '12px',
39
- color: colors.textSecondary,
40
- marginTop: '4px',
41
- }}
42
- >
43
- {resource.name}
44
- </div>
45
- )}
46
- {resource.description && (
33
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
47
34
  <div
48
35
  style={{
49
- fontSize: '12px',
50
- color: colors.textSecondary,
51
- 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',
52
43
  }}
53
- >
54
- {resource.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: resource.name || resource.description ? '6px' : '0',
52
+ lineHeight: '1.4',
53
+ wordBreak: 'break-word',
54
+ }}
55
+ >
56
+ {resource.uri}
57
+ </div>
58
+ {resource.name && (
59
+ <div
60
+ style={{
61
+ fontSize: '13px',
62
+ color: colors.textPrimary,
63
+ fontWeight: '500',
64
+ lineHeight: '1.5',
65
+ marginTop: '4px',
66
+ }}
67
+ >
68
+ {resource.name}
69
+ </div>
70
+ )}
71
+ {resource.description && (
72
+ <div
73
+ style={{
74
+ fontSize: '12px',
75
+ color: colors.textSecondary,
76
+ lineHeight: '1.5',
77
+ marginTop: resource.name ? '2px' : '4px',
78
+ }}
79
+ >
80
+ {resource.description}
81
+ </div>
82
+ )}
55
83
  </div>
56
- )}
84
+ </div>
57
85
  </div>
58
86
  );
59
87
  }
@@ -31,14 +31,16 @@ export default function ResourcesList({
31
31
  ) : resources.length === 0 ? (
32
32
  <EmptyState message="No resources available." />
33
33
  ) : (
34
- resources.map((resource, idx) => (
35
- <ResourceItem
36
- key={idx}
37
- resource={resource}
38
- isSelected={selectedResource?.uri === resource.uri}
39
- onClick={() => onSelectResource(resource)}
40
- />
41
- ))
34
+ <div style={{ padding: '8px 0' }}>
35
+ {resources.map((resource, idx) => (
36
+ <ResourceItem
37
+ key={idx}
38
+ resource={resource}
39
+ isSelected={selectedResource?.uri === resource.uri}
40
+ onClick={() => onSelectResource(resource)}
41
+ />
42
+ ))}
43
+ </div>
42
44
  )}
43
45
  </div>
44
46
  );
@@ -5,44 +5,69 @@ export default function ToolItem({ tool, 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
- {tool.name}
34
- </div>
35
- {tool.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
- {tool.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: tool.description ? '6px' : '0',
52
+ lineHeight: '1.4',
53
+ }}
54
+ >
55
+ {tool.name}
56
+ </div>
57
+ {tool.description && (
58
+ <div
59
+ style={{
60
+ fontSize: '12px',
61
+ color: colors.textSecondary,
62
+ lineHeight: '1.5',
63
+ marginTop: '4px',
64
+ }}
65
+ >
66
+ {tool.description}
67
+ </div>
68
+ )}
44
69
  </div>
45
- )}
70
+ </div>
46
71
  </div>
47
72
  );
48
73
  }
@@ -31,14 +31,16 @@ export default function ToolsList({
31
31
  ) : tools.length === 0 ? (
32
32
  <EmptyState message="No tools available." />
33
33
  ) : (
34
- tools.map((tool, idx) => (
35
- <ToolItem
36
- key={idx}
37
- tool={tool}
38
- isSelected={selectedTool?.name === tool.name}
39
- onClick={() => onSelectTool(tool)}
40
- />
41
- ))
34
+ <div style={{ padding: '8px 0' }}>
35
+ {tools.map((tool, idx) => (
36
+ <ToolItem
37
+ key={idx}
38
+ tool={tool}
39
+ isSelected={selectedTool?.name === tool.name}
40
+ onClick={() => onSelectTool(tool)}
41
+ />
42
+ ))}
43
+ </div>
42
44
  )}
43
45
  </div>
44
46
  );