@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
@@ -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
  );
@@ -0,0 +1,107 @@
1
+ import { useState } from 'react';
2
+
3
+ export function useMcpDataLoader(makeMcpRequest, selectedServer, setError) {
4
+ const [tools, setTools] = useState([]);
5
+ const [prompts, setPrompts] = useState([]);
6
+ const [resources, setResources] = useState([]);
7
+ const [toolsLoading, setToolsLoading] = useState(false);
8
+ const [promptsLoading, setPromptsLoading] = useState(false);
9
+ const [resourcesLoading, setResourcesLoading] = useState(false);
10
+ const [toolsLoaded, setToolsLoaded] = useState(false);
11
+ const [promptsLoaded, setPromptsLoaded] = useState(false);
12
+ const [resourcesLoaded, setResourcesLoaded] = useState(false);
13
+
14
+ const loadTools = async () => {
15
+ if (!selectedServer) {
16
+ setError('tools: No server selected');
17
+ setToolsLoaded(true);
18
+ return;
19
+ }
20
+
21
+ setToolsLoading(true);
22
+ setError(null);
23
+ try {
24
+ const result = await makeMcpRequest('tools/list');
25
+ setTools(result?.tools || []);
26
+ setToolsLoaded(true);
27
+ } catch (err) {
28
+ const errorMsg = err.message || 'Failed to load tools';
29
+ setError(`tools: ${errorMsg}`);
30
+ setToolsLoaded(true);
31
+ console.error('Failed to load tools:', err);
32
+ } finally {
33
+ setToolsLoading(false);
34
+ }
35
+ };
36
+
37
+ const loadPrompts = async () => {
38
+ if (!selectedServer) {
39
+ setError('prompts: No server selected');
40
+ setPromptsLoaded(true);
41
+ return;
42
+ }
43
+
44
+ setPromptsLoading(true);
45
+ setError(null);
46
+ try {
47
+ const result = await makeMcpRequest('prompts/list');
48
+ setPrompts(result?.prompts || []);
49
+ setPromptsLoaded(true);
50
+ } catch (err) {
51
+ const errorMsg = err.message || 'Failed to load prompts';
52
+ setError(`prompts: ${errorMsg}`);
53
+ setPromptsLoaded(true);
54
+ console.error('Failed to load prompts:', err);
55
+ } finally {
56
+ setPromptsLoading(false);
57
+ }
58
+ };
59
+
60
+ const loadResources = async () => {
61
+ if (!selectedServer) {
62
+ setError('resources: No server selected');
63
+ setResourcesLoaded(true);
64
+ return;
65
+ }
66
+
67
+ setResourcesLoading(true);
68
+ setError(null);
69
+ try {
70
+ const result = await makeMcpRequest('resources/list');
71
+ setResources(result?.resources || []);
72
+ setResourcesLoaded(true);
73
+ } catch (err) {
74
+ const errorMsg = err.message || 'Failed to load resources';
75
+ setError(`resources: ${errorMsg}`);
76
+ setResourcesLoaded(true);
77
+ console.error('Failed to load resources:', err);
78
+ } finally {
79
+ setResourcesLoading(false);
80
+ }
81
+ };
82
+
83
+ const resetData = () => {
84
+ setToolsLoaded(false);
85
+ setPromptsLoaded(false);
86
+ setResourcesLoaded(false);
87
+ setTools([]);
88
+ setPrompts([]);
89
+ setResources([]);
90
+ };
91
+
92
+ return {
93
+ tools,
94
+ prompts,
95
+ resources,
96
+ toolsLoading,
97
+ promptsLoading,
98
+ resourcesLoading,
99
+ toolsLoaded,
100
+ promptsLoaded,
101
+ resourcesLoaded,
102
+ loadTools,
103
+ loadPrompts,
104
+ loadResources,
105
+ resetData,
106
+ };
107
+ }
@@ -0,0 +1,65 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export function useMcpRequest(selectedServer) {
4
+ const [loading, setLoading] = useState(false);
5
+ const [error, setError] = useState(null);
6
+ const [sessionId, setSessionId] = useState(null);
7
+
8
+ const makeMcpRequest = useCallback(
9
+ async (method, params = {}) => {
10
+ if (!selectedServer) {
11
+ throw new Error('No server selected');
12
+ }
13
+
14
+ setError(null);
15
+ setLoading(true);
16
+
17
+ try {
18
+ const headers = { 'Content-Type': 'application/json' };
19
+ if (sessionId) {
20
+ headers['Mcp-Session-Id'] = sessionId;
21
+ }
22
+
23
+ const response = await fetch('/api/playground/proxy', {
24
+ method: 'POST',
25
+ headers,
26
+ body: JSON.stringify({ method, params, serverName: selectedServer }),
27
+ });
28
+
29
+ const data = await response.json();
30
+
31
+ const responseSessionId =
32
+ response.headers.get('Mcp-Session-Id') ||
33
+ response.headers.get('mcp-session-id') ||
34
+ data._sessionId;
35
+ if (responseSessionId && responseSessionId !== sessionId) {
36
+ setSessionId(responseSessionId);
37
+ }
38
+
39
+ if (!response.ok) {
40
+ throw new Error(data.error?.message || data.message || 'Request failed');
41
+ }
42
+
43
+ return data.result || data;
44
+ } catch (err) {
45
+ setError(err.message);
46
+ throw err;
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ },
51
+ [selectedServer, sessionId]
52
+ );
53
+
54
+ const resetSession = useCallback(() => {
55
+ setSessionId(null);
56
+ }, []);
57
+
58
+ return {
59
+ makeMcpRequest,
60
+ loading,
61
+ error,
62
+ setError,
63
+ resetSession,
64
+ };
65
+ }
@@ -0,0 +1,70 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ export function useMcpServerStatus() {
4
+ const [serverStatus, setServerStatus] = useState(null);
5
+ const [showLoadingModal, setShowLoadingModal] = useState(false);
6
+ const [availableServers, setAvailableServers] = useState([]);
7
+ const [selectedServer, setSelectedServer] = useState(null);
8
+
9
+ const checkServerStatus = async () => {
10
+ try {
11
+ const res = await fetch('/api/composite/status');
12
+ if (!res.ok) {
13
+ throw new Error('Server not available');
14
+ }
15
+ const data = await res.json();
16
+ const wasRunning = serverStatus?.running;
17
+ setServerStatus(data);
18
+
19
+ if (!data.running) {
20
+ if (!showLoadingModal || wasRunning) {
21
+ setShowLoadingModal(true);
22
+ }
23
+ } else if (data.running && showLoadingModal) {
24
+ setShowLoadingModal(false);
25
+ }
26
+ } catch (err) {
27
+ setServerStatus({ running: false });
28
+ if (!showLoadingModal) {
29
+ setShowLoadingModal(true);
30
+ }
31
+ }
32
+ };
33
+
34
+ const loadAvailableServers = async () => {
35
+ try {
36
+ const res = await fetch('/api/composite/servers');
37
+ if (res.ok) {
38
+ const data = await res.json();
39
+ setAvailableServers(data.servers || []);
40
+ if (data.servers && data.servers.length > 0 && !selectedServer) {
41
+ setSelectedServer(data.servers[0]);
42
+ }
43
+ }
44
+ } catch (err) {
45
+ console.error('Failed to load servers:', err);
46
+ }
47
+ };
48
+
49
+ useEffect(() => {
50
+ checkServerStatus();
51
+ loadAvailableServers();
52
+ const interval = setInterval(checkServerStatus, 2000);
53
+ return () => clearInterval(interval);
54
+ }, []);
55
+
56
+ useEffect(() => {
57
+ if (availableServers.length > 0 && !selectedServer) {
58
+ setSelectedServer(availableServers[0]);
59
+ }
60
+ }, [availableServers, selectedServer]);
61
+
62
+ return {
63
+ serverStatus,
64
+ showLoadingModal,
65
+ availableServers,
66
+ selectedServer,
67
+ setSelectedServer,
68
+ checkServerStatus,
69
+ };
70
+ }