@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.
- 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 +1 -1
- 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
|
@@ -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
|
);
|
|
@@ -5,55 +5,83 @@ export default function ResourceItem({ resource, 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
|
-
{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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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: '
|
|
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
|
-
{tool.name}
|
|
34
|
-
</div>
|
|
35
|
-
{tool.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: 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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
);
|