@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
@@ -1,12 +1,10 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { useMcpRequest } from './hooks/useMcpRequest';
3
+ import { useMcpServerStatus } from './hooks/useMcpServerStatus';
4
+ import { useMcpDataLoader } from './hooks/useMcpDataLoader';
2
5
 
3
6
  export function useMcpPlayground() {
4
7
  const [activeSection, setActiveSection] = useState('tools');
5
- const [tools, setTools] = useState([]);
6
- const [prompts, setPrompts] = useState([]);
7
- const [resources, setResources] = useState([]);
8
- const [loading, setLoading] = useState(false);
9
- const [error, setError] = useState(null);
10
8
  const [selectedTool, setSelectedTool] = useState(null);
11
9
  const [toolArgs, setToolArgs] = useState('{}');
12
10
  const [toolResult, setToolResult] = useState(null);
@@ -15,33 +13,77 @@ export function useMcpPlayground() {
15
13
  const [promptResult, setPromptResult] = useState(null);
16
14
  const [selectedResource, setSelectedResource] = useState(null);
17
15
  const [resourceResult, setResourceResult] = useState(null);
18
- const [serverStatus, setServerStatus] = useState(null);
19
- const [sessionId, setSessionId] = useState(null);
20
- const [showLoadingModal, setShowLoadingModal] = useState(false);
21
- const [toolsLoading, setToolsLoading] = useState(false);
22
- const [promptsLoading, setPromptsLoading] = useState(false);
23
- const [resourcesLoading, setResourcesLoading] = useState(false);
24
- const [toolsLoaded, setToolsLoaded] = useState(false);
25
- const [promptsLoaded, setPromptsLoaded] = useState(false);
26
- const [resourcesLoaded, setResourcesLoaded] = useState(false);
27
16
 
17
+ const { serverStatus, showLoadingModal, availableServers, selectedServer, setSelectedServer } =
18
+ useMcpServerStatus();
19
+
20
+ const { makeMcpRequest, loading, error, setError, resetSession } = useMcpRequest(selectedServer);
21
+
22
+ const {
23
+ tools,
24
+ prompts,
25
+ resources,
26
+ toolsLoading,
27
+ promptsLoading,
28
+ resourcesLoading,
29
+ toolsLoaded,
30
+ promptsLoaded,
31
+ resourcesLoaded,
32
+ loadTools,
33
+ loadPrompts,
34
+ loadResources,
35
+ resetData,
36
+ } = useMcpDataLoader(makeMcpRequest, selectedServer, setError);
37
+
38
+ // Reset and reload data when server changes
28
39
  useEffect(() => {
29
- checkServerStatus();
30
- const interval = setInterval(checkServerStatus, 2000);
31
- return () => clearInterval(interval);
32
- }, []);
40
+ if (!selectedServer || !serverStatus?.running) {
41
+ return;
42
+ }
43
+
44
+ resetData();
45
+ setSelectedTool(null);
46
+ setSelectedPrompt(null);
47
+ setSelectedResource(null);
48
+ setToolResult(null);
49
+ setPromptResult(null);
50
+ setResourceResult(null);
51
+ setToolArgs('{}');
52
+ setPromptArgs('{}');
53
+ resetSession();
54
+ setError(null);
55
+
56
+ const timer = setTimeout(() => {
57
+ if (activeSection === 'tools') {
58
+ loadTools();
59
+ } else if (activeSection === 'prompts') {
60
+ loadPrompts();
61
+ } else if (activeSection === 'resources') {
62
+ loadResources();
63
+ }
64
+ }, 100);
65
+
66
+ return () => clearTimeout(timer);
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ }, [selectedServer, serverStatus?.running, activeSection]);
33
69
 
34
70
  useEffect(() => {
35
- if (serverStatus?.running && activeSection === 'tools' && tools.length === 0) {
71
+ if (
72
+ serverStatus?.running &&
73
+ activeSection === 'tools' &&
74
+ tools.length === 0 &&
75
+ selectedServer
76
+ ) {
36
77
  const timer = setTimeout(() => {
37
78
  loadTools();
38
79
  }, 2000);
39
80
  return () => clearTimeout(timer);
40
81
  }
41
- }, [serverStatus?.running]);
82
+ // eslint-disable-next-line react-hooks/exhaustive-deps
83
+ }, [serverStatus?.running, selectedServer, activeSection, tools.length]);
42
84
 
43
85
  useEffect(() => {
44
- if (!serverStatus?.running) return;
86
+ if (!serverStatus?.running || !selectedServer) return;
45
87
 
46
88
  const timer = setTimeout(() => {
47
89
  if (activeSection === 'tools' && !toolsLoaded && !toolsLoading) {
@@ -54,9 +96,11 @@ export function useMcpPlayground() {
54
96
  }, 100);
55
97
 
56
98
  return () => clearTimeout(timer);
99
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
100
  }, [
58
101
  activeSection,
59
102
  serverStatus?.running,
103
+ selectedServer,
60
104
  toolsLoaded,
61
105
  promptsLoaded,
62
106
  resourcesLoaded,
@@ -65,122 +109,6 @@ export function useMcpPlayground() {
65
109
  resourcesLoading,
66
110
  ]);
67
111
 
68
- const checkServerStatus = async () => {
69
- try {
70
- const res = await fetch('/api/composite/status');
71
- if (!res.ok) {
72
- throw new Error('Server not available');
73
- }
74
- const data = await res.json();
75
- const wasRunning = serverStatus?.running;
76
- setServerStatus(data);
77
-
78
- if (!data.running) {
79
- if (!showLoadingModal || wasRunning) {
80
- setShowLoadingModal(true);
81
- }
82
- } else if (data.running && showLoadingModal) {
83
- setShowLoadingModal(false);
84
- }
85
- } catch (err) {
86
- // Silently handle connection errors - server is not running
87
- setServerStatus({ running: false });
88
- if (!showLoadingModal) {
89
- setShowLoadingModal(true);
90
- }
91
- }
92
- };
93
-
94
- const makeMcpRequest = async (method, params = {}) => {
95
- setError(null);
96
- setLoading(true);
97
-
98
- try {
99
- const headers = { 'Content-Type': 'application/json' };
100
- if (sessionId) {
101
- headers['Mcp-Session-Id'] = sessionId;
102
- }
103
-
104
- const response = await fetch('/api/playground/proxy', {
105
- method: 'POST',
106
- headers,
107
- body: JSON.stringify({ method, params }),
108
- });
109
-
110
- const data = await response.json();
111
-
112
- const responseSessionId =
113
- response.headers.get('Mcp-Session-Id') ||
114
- response.headers.get('mcp-session-id') ||
115
- data._sessionId;
116
- if (responseSessionId && responseSessionId !== sessionId) {
117
- setSessionId(responseSessionId);
118
- }
119
-
120
- if (!response.ok) {
121
- throw new Error(data.error?.message || data.message || 'Request failed');
122
- }
123
-
124
- return data.result || data;
125
- } catch (err) {
126
- setError(err.message);
127
- throw err;
128
- } finally {
129
- setLoading(false);
130
- }
131
- };
132
-
133
- const loadTools = async () => {
134
- setToolsLoading(true);
135
- setError(null);
136
- try {
137
- const result = await makeMcpRequest('tools/list');
138
- setTools(result?.tools || []);
139
- setToolsLoaded(true);
140
- } catch (err) {
141
- const errorMsg = err.message || 'Failed to load tools';
142
- setError(`tools: ${errorMsg}`);
143
- setToolsLoaded(true);
144
- console.error('Failed to load tools:', err);
145
- } finally {
146
- setToolsLoading(false);
147
- }
148
- };
149
-
150
- const loadPrompts = async () => {
151
- setPromptsLoading(true);
152
- setError(null);
153
- try {
154
- const result = await makeMcpRequest('prompts/list');
155
- setPrompts(result?.prompts || []);
156
- setPromptsLoaded(true);
157
- } catch (err) {
158
- const errorMsg = err.message || 'Failed to load prompts';
159
- setError(`prompts: ${errorMsg}`);
160
- setPromptsLoaded(true);
161
- console.error('Failed to load prompts:', err);
162
- } finally {
163
- setPromptsLoading(false);
164
- }
165
- };
166
-
167
- const loadResources = async () => {
168
- setResourcesLoading(true);
169
- setError(null);
170
- try {
171
- const result = await makeMcpRequest('resources/list');
172
- setResources(result?.resources || []);
173
- setResourcesLoaded(true);
174
- } catch (err) {
175
- const errorMsg = err.message || 'Failed to load resources';
176
- setError(`resources: ${errorMsg}`);
177
- setResourcesLoaded(true);
178
- console.error('Failed to load resources:', err);
179
- } finally {
180
- setResourcesLoading(false);
181
- }
182
- };
183
-
184
112
  const handleCallTool = async () => {
185
113
  if (!selectedTool) return;
186
114
 
@@ -276,5 +204,8 @@ export function useMcpPlayground() {
276
204
  handleCallTool,
277
205
  handleGetPrompt,
278
206
  handleReadResource,
207
+ availableServers,
208
+ selectedServer,
209
+ setSelectedServer,
279
210
  };
280
211
  }
@@ -41,6 +41,9 @@ function McpPlayground() {
41
41
  handleCallTool,
42
42
  handleGetPrompt,
43
43
  handleReadResource,
44
+ availableServers,
45
+ selectedServer,
46
+ setSelectedServer,
44
47
  } = useMcpPlayground();
45
48
 
46
49
  return (
@@ -84,32 +87,108 @@ function McpPlayground() {
84
87
  <div
85
88
  style={{
86
89
  display: 'flex',
87
- gap: '8px',
88
- borderBottom: `1px solid ${colors.borderLight}`,
90
+ flexDirection: 'column',
91
+ gap: '12px',
89
92
  }}
90
93
  >
91
- {['tools', 'prompts', 'resources'].map((section) => (
92
- <button
93
- key={section}
94
- onClick={() => setActiveSection(section)}
94
+ {availableServers.length > 0 && (
95
+ <div
95
96
  style={{
96
- padding: '10px 18px',
97
- background: activeSection === section ? colors.bgSecondary : 'transparent',
98
- border: 'none',
99
- borderBottom: `2px solid ${activeSection === section ? colors.accentBlue : 'transparent'}`,
100
- color: activeSection === section ? colors.textPrimary : colors.textSecondary,
101
- cursor: 'pointer',
102
- fontSize: '13px',
103
- fontFamily: fonts.body,
104
- fontWeight: activeSection === section ? '500' : '400',
105
- textTransform: 'capitalize',
106
- borderRadius: '6px 6px 0 0',
107
- transition: 'all 0.2s',
97
+ display: 'flex',
98
+ flexDirection: 'column',
99
+ gap: '8px',
108
100
  }}
109
101
  >
110
- {section}
111
- </button>
112
- ))}
102
+ <label
103
+ style={{
104
+ fontSize: '13px',
105
+ fontFamily: fonts.body,
106
+ color: colors.textSecondary,
107
+ fontWeight: '500',
108
+ }}
109
+ >
110
+ Server:
111
+ </label>
112
+ <div
113
+ style={{
114
+ display: 'flex',
115
+ flexWrap: 'wrap',
116
+ gap: '8px',
117
+ }}
118
+ >
119
+ {availableServers.map((server) => (
120
+ <button
121
+ key={server}
122
+ onClick={() => setSelectedServer(server)}
123
+ style={{
124
+ padding: '10px 18px',
125
+ background:
126
+ selectedServer === server ? colors.accentBlue : colors.bgSecondary,
127
+ border:
128
+ selectedServer === server
129
+ ? `2px solid ${colors.accentBlue}`
130
+ : `1px solid ${colors.borderLight}`,
131
+ borderRadius: '8px',
132
+ color: selectedServer === server ? colors.textInverse : colors.textPrimary,
133
+ fontSize: '13px',
134
+ fontFamily: fonts.body,
135
+ fontWeight: selectedServer === server ? '600' : '500',
136
+ cursor: 'pointer',
137
+ transition: 'all 0.2s ease',
138
+ boxShadow:
139
+ selectedServer === server ? `0 2px 4px ${colors.shadowSm}` : 'none',
140
+ }}
141
+ onMouseEnter={(e) => {
142
+ if (selectedServer !== server) {
143
+ e.currentTarget.style.background = colors.bgHover;
144
+ e.currentTarget.style.borderColor = colors.borderMedium;
145
+ e.currentTarget.style.boxShadow = `0 2px 4px ${colors.shadowSm}`;
146
+ }
147
+ }}
148
+ onMouseLeave={(e) => {
149
+ if (selectedServer !== server) {
150
+ e.currentTarget.style.background = colors.bgSecondary;
151
+ e.currentTarget.style.borderColor = colors.borderLight;
152
+ e.currentTarget.style.boxShadow = 'none';
153
+ }
154
+ }}
155
+ >
156
+ {server}
157
+ </button>
158
+ ))}
159
+ </div>
160
+ </div>
161
+ )}
162
+ <div
163
+ style={{
164
+ display: 'flex',
165
+ gap: '8px',
166
+ borderBottom: `1px solid ${colors.borderLight}`,
167
+ }}
168
+ >
169
+ {['tools', 'prompts', 'resources'].map((section) => (
170
+ <button
171
+ key={section}
172
+ onClick={() => setActiveSection(section)}
173
+ style={{
174
+ padding: '10px 18px',
175
+ background: activeSection === section ? colors.bgSecondary : 'transparent',
176
+ border: 'none',
177
+ borderBottom: `2px solid ${activeSection === section ? colors.accentBlue : 'transparent'}`,
178
+ color: activeSection === section ? colors.textPrimary : colors.textSecondary,
179
+ cursor: 'pointer',
180
+ fontSize: '13px',
181
+ fontFamily: fonts.body,
182
+ fontWeight: activeSection === section ? '500' : '400',
183
+ textTransform: 'capitalize',
184
+ borderRadius: '6px 6px 0 0',
185
+ transition: 'all 0.2s',
186
+ }}
187
+ >
188
+ {section}
189
+ </button>
190
+ ))}
191
+ </div>
113
192
  </div>
114
193
 
115
194
  <div style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
@@ -1,3 +1,4 @@
1
+ const LLM_SERVER = 'LLM Server';
1
2
  export function extractServerName(request) {
2
3
  if (request.body_json) {
3
4
  try {
@@ -59,13 +60,13 @@ export function formatDateTime(timestampISO) {
59
60
  export function getSourceDest(request) {
60
61
  if (request.direction === 'request') {
61
62
  return {
62
- source: request.remote_address || 'Client',
63
- dest: request.host || 'Server',
63
+ source: LLM_SERVER,
64
+ dest: request.remote_address || 'Unknown MCP Client',
64
65
  };
65
66
  }
66
67
  return {
67
- source: request.host || 'Server',
68
- dest: request.remote_address || 'Client',
68
+ source: request.remote_address || 'Unknown MCP Server',
69
+ dest: LLM_SERVER,
69
70
  };
70
71
  }
71
72