@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
|
@@ -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
|
);
|
|
@@ -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
|
+
}
|