@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.
- package/README.md +16 -1
- package/bin/mcp-shark.js +179 -53
- 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 +4 -2
- 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
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
90
|
+
flexDirection: 'column',
|
|
91
|
+
gap: '12px',
|
|
89
92
|
}}
|
|
90
93
|
>
|
|
91
|
-
{
|
|
92
|
-
<
|
|
93
|
-
key={section}
|
|
94
|
-
onClick={() => setActiveSection(section)}
|
|
94
|
+
{availableServers.length > 0 && (
|
|
95
|
+
<div
|
|
95
96
|
style={{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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:
|
|
63
|
-
dest: request.
|
|
63
|
+
source: LLM_SERVER,
|
|
64
|
+
dest: request.remote_address || 'Unknown MCP Client',
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
return {
|
|
67
|
-
source: request.
|
|
68
|
-
dest:
|
|
68
|
+
source: request.remote_address || 'Unknown MCP Server',
|
|
69
|
+
dest: LLM_SERVER,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
|