@mcp-shark/mcp-shark 1.5.8 → 1.5.9

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.
@@ -7,7 +7,7 @@
7
7
  <title>MCP Shark</title>
8
8
  <link rel="icon" type="image/png" href="/og-image.png" />
9
9
  <link rel="apple-touch-icon" href="/og-image.png" />
10
- <script type="module" crossorigin src="/assets/index-CArYxKxS.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-D1MNKhKd.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-Cc-IUa83.css">
12
12
  </head>
13
13
  <body>
@@ -39,6 +39,9 @@ export class ServerManagementController {
39
39
  fileContent,
40
40
  selectedServices,
41
41
  port: 9851,
42
+ onLog: (type, message) => {
43
+ this._addLogEntry(type, message);
44
+ },
42
45
  onError: (err) => {
43
46
  this._addLogEntry('error', `Failed to start mcp-shark server: ${err.message}`);
44
47
  throw err;
package/ui/src/App.jsx CHANGED
@@ -2,10 +2,10 @@ import { useEffect, useState } from 'react';
2
2
  import CompositeLogs from './CompositeLogs';
3
3
  import CompositeSetup from './CompositeSetup';
4
4
  import IntroTour from './IntroTour';
5
+ import ShutdownPage from './ShutdownPage';
5
6
  import SmartScan from './SmartScan';
6
7
  import TabNavigation from './TabNavigation';
7
- import ApiDocsButton from './components/App/ApiDocsButton';
8
- import HelpButton from './components/App/HelpButton';
8
+ import ActionMenu from './components/App/ActionMenu';
9
9
  import TrafficTab from './components/App/TrafficTab';
10
10
  import { useAppState } from './components/App/useAppState';
11
11
  import McpPlayground from './components/McpPlayground';
@@ -84,9 +84,8 @@ function App() {
84
84
  <div style={{ position: 'relative' }} data-tour="tabs">
85
85
  <TabNavigation activeTab={activeTab} onTabChange={setActiveTab} />
86
86
  </div>
87
- <ApiDocsButton />
88
- <HelpButton
89
- onClick={() => {
87
+ <ActionMenu
88
+ onHelpClick={() => {
90
89
  if (showTour) {
91
90
  setShowTour(false);
92
91
  setTourKey((prev) => prev + 1);
@@ -148,6 +147,15 @@ function App() {
148
147
  <SmartScan />
149
148
  </div>
150
149
  )}
150
+
151
+ {activeTab === 'shutdown' && (
152
+ <div
153
+ data-tab-content
154
+ style={{ flex: 1, overflow: 'hidden', width: '100%', height: '100%' }}
155
+ >
156
+ <ShutdownPage />
157
+ </div>
158
+ )}
151
159
  </div>
152
160
  );
153
161
  }
@@ -0,0 +1,91 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { colors, fonts } from './theme';
3
+
4
+ /**
5
+ * Shutdown page - shows message and redirects to root after 3 seconds
6
+ */
7
+ function ShutdownPage() {
8
+ const [countdown, setCountdown] = useState(3);
9
+
10
+ useEffect(() => {
11
+ const interval = setInterval(() => {
12
+ setCountdown((prev) => {
13
+ if (prev <= 1) {
14
+ clearInterval(interval);
15
+ window.location.href = '/';
16
+ return 0;
17
+ }
18
+ return prev - 1;
19
+ });
20
+ }, 1000);
21
+
22
+ return () => clearInterval(interval);
23
+ }, []);
24
+
25
+ return (
26
+ <div
27
+ style={{
28
+ display: 'flex',
29
+ flexDirection: 'column',
30
+ alignItems: 'center',
31
+ justifyContent: 'center',
32
+ height: '100vh',
33
+ background: colors.bgPrimary,
34
+ fontFamily: fonts.body,
35
+ padding: '20px',
36
+ textAlign: 'center',
37
+ }}
38
+ >
39
+ <div
40
+ style={{
41
+ maxWidth: '500px',
42
+ width: '100%',
43
+ }}
44
+ >
45
+ <h1
46
+ style={{
47
+ fontSize: '32px',
48
+ fontWeight: '600',
49
+ color: colors.textPrimary,
50
+ margin: '0 0 16px 0',
51
+ }}
52
+ >
53
+ MCP Shark Shutdown
54
+ </h1>
55
+ <p
56
+ style={{
57
+ fontSize: '16px',
58
+ color: colors.textSecondary,
59
+ margin: '0 0 32px 0',
60
+ lineHeight: '1.6',
61
+ }}
62
+ >
63
+ MCP Shark has been shut down.
64
+ </p>
65
+ <div
66
+ style={{
67
+ padding: '24px',
68
+ background: colors.bgCard,
69
+ borderRadius: '12px',
70
+ border: `1px solid ${colors.borderLight}`,
71
+ boxShadow: `0 4px 12px ${colors.shadowMd}`,
72
+ }}
73
+ >
74
+ <p
75
+ style={{
76
+ fontSize: '14px',
77
+ color: colors.textSecondary,
78
+ margin: '0 0 16px 0',
79
+ lineHeight: '1.6',
80
+ }}
81
+ >
82
+ The MCP Shark server has been stopped. Redirecting in {countdown} second
83
+ {countdown !== 1 ? 's' : ''}...
84
+ </p>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ export default ShutdownPage;
@@ -0,0 +1,179 @@
1
+ import { IconMenu2, IconX } from '@tabler/icons-react';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { colors, fonts } from '../../theme';
4
+ import ApiDocsButton from './ApiDocsButton';
5
+ import HelpButton from './HelpButton';
6
+ import ShutdownButton from './ShutdownButton';
7
+
8
+ /**
9
+ * Expandable action menu grouping API, Help, and Shutdown buttons
10
+ */
11
+ export default function ActionMenu({ onHelpClick }) {
12
+ const [isExpanded, setIsExpanded] = useState(false);
13
+ const menuRef = useRef(null);
14
+
15
+ // Close menu when clicking outside
16
+ useEffect(() => {
17
+ const handleClickOutside = (event) => {
18
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
19
+ setIsExpanded(false);
20
+ }
21
+ };
22
+
23
+ if (isExpanded) {
24
+ document.addEventListener('mousedown', handleClickOutside);
25
+ return () => document.removeEventListener('mousedown', handleClickOutside);
26
+ }
27
+ }, [isExpanded]);
28
+
29
+ const buttonStyle = {
30
+ position: 'fixed',
31
+ bottom: '20px',
32
+ right: '20px',
33
+ background: colors.bgCard,
34
+ border: `1px solid ${colors.borderLight}`,
35
+ borderRadius: '50%',
36
+ width: '48px',
37
+ height: '48px',
38
+ padding: 0,
39
+ color: colors.textSecondary,
40
+ cursor: 'pointer',
41
+ fontFamily: fonts.body,
42
+ boxShadow: `0 4px 12px ${colors.shadowMd}`,
43
+ display: 'flex',
44
+ alignItems: 'center',
45
+ justifyContent: 'center',
46
+ zIndex: 9999,
47
+ transition: 'all 0.2s ease',
48
+ };
49
+
50
+ const menuItemStyle = {
51
+ position: 'absolute',
52
+ right: '0',
53
+ background: colors.bgCard,
54
+ border: `1px solid ${colors.borderLight}`,
55
+ borderRadius: '50%',
56
+ width: '48px',
57
+ height: '48px',
58
+ padding: 0,
59
+ color: colors.textSecondary,
60
+ cursor: 'pointer',
61
+ fontFamily: fonts.body,
62
+ boxShadow: `0 4px 12px ${colors.shadowMd}`,
63
+ display: 'flex',
64
+ alignItems: 'center',
65
+ justifyContent: 'center',
66
+ transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
67
+ opacity: isExpanded ? 1 : 0,
68
+ pointerEvents: isExpanded ? 'auto' : 'none',
69
+ transform: isExpanded ? 'scale(1) translateY(0)' : 'scale(0.8) translateY(10px)',
70
+ };
71
+
72
+ return (
73
+ <div ref={menuRef} style={{ position: 'fixed', bottom: '20px', right: '20px', zIndex: 9999 }}>
74
+ {/* API Docs Button - Top */}
75
+ <ApiDocsButton
76
+ style={{
77
+ ...menuItemStyle,
78
+ bottom: isExpanded ? '180px' : '0',
79
+ position: 'absolute',
80
+ left: 'auto', // Ensure no left positioning conflicts
81
+ }}
82
+ onClick={() => setIsExpanded(false)}
83
+ onMouseEnter={(e) => {
84
+ if (isExpanded) {
85
+ e.currentTarget.style.transform = 'scale(1.1) translateY(0)';
86
+ e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
87
+ }
88
+ }}
89
+ onMouseLeave={(e) => {
90
+ if (isExpanded) {
91
+ e.currentTarget.style.transform = 'scale(1) translateY(0)';
92
+ e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
93
+ }
94
+ }}
95
+ />
96
+
97
+ {/* Help Button - Middle */}
98
+ <HelpButton
99
+ onClick={() => {
100
+ onHelpClick();
101
+ setIsExpanded(false);
102
+ }}
103
+ style={{
104
+ ...menuItemStyle,
105
+ bottom: isExpanded ? '120px' : '0',
106
+ position: 'absolute',
107
+ left: 'auto', // Ensure no left positioning conflicts
108
+ }}
109
+ onMouseEnter={(e) => {
110
+ if (isExpanded) {
111
+ e.currentTarget.style.transform = 'scale(1.1) translateY(0)';
112
+ e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
113
+ }
114
+ }}
115
+ onMouseLeave={(e) => {
116
+ if (isExpanded) {
117
+ e.currentTarget.style.transform = 'scale(1) translateY(0)';
118
+ e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
119
+ }
120
+ }}
121
+ />
122
+
123
+ {/* Shutdown Button - Bottom (closest to main button) */}
124
+ <ShutdownButton
125
+ style={{
126
+ ...menuItemStyle,
127
+ bottom: isExpanded ? '60px' : '0',
128
+ position: 'absolute',
129
+ left: 'auto', // Override default left positioning
130
+ }}
131
+ onClick={() => setIsExpanded(false)}
132
+ onMouseEnter={(e) => {
133
+ if (isExpanded) {
134
+ e.currentTarget.style.transform = 'scale(1.1) translateY(0)';
135
+ e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
136
+ }
137
+ }}
138
+ onMouseLeave={(e) => {
139
+ if (isExpanded) {
140
+ e.currentTarget.style.transform = 'scale(1) translateY(0)';
141
+ e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
142
+ }
143
+ }}
144
+ />
145
+
146
+ {/* Main Toggle Button */}
147
+ <button
148
+ type="button"
149
+ onClick={() => setIsExpanded(!isExpanded)}
150
+ style={{
151
+ ...buttonStyle,
152
+ background: isExpanded ? colors.accentBlue : colors.bgCard,
153
+ color: isExpanded ? colors.textInverse : colors.textSecondary,
154
+ }}
155
+ onMouseEnter={(e) => {
156
+ if (!isExpanded) {
157
+ e.currentTarget.style.background = colors.bgHover;
158
+ e.currentTarget.style.color = colors.textPrimary;
159
+ e.currentTarget.style.borderColor = colors.borderMedium;
160
+ e.currentTarget.style.transform = 'scale(1.1)';
161
+ e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
162
+ }
163
+ }}
164
+ onMouseLeave={(e) => {
165
+ if (!isExpanded) {
166
+ e.currentTarget.style.background = colors.bgCard;
167
+ e.currentTarget.style.color = colors.textSecondary;
168
+ e.currentTarget.style.borderColor = colors.borderLight;
169
+ e.currentTarget.style.transform = 'scale(1)';
170
+ e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
171
+ }
172
+ }}
173
+ title={isExpanded ? 'Close menu' : 'Open menu'}
174
+ >
175
+ {isExpanded ? <IconX size={20} stroke={1.5} /> : <IconMenu2 size={20} stroke={1.5} />}
176
+ </button>
177
+ </div>
178
+ );
179
+ }
@@ -4,10 +4,62 @@ import { colors, fonts } from '../../theme';
4
4
  /**
5
5
  * API Documentation button component
6
6
  * Opens Swagger/OpenAPI documentation in a new tab
7
+ * @param {Object} props
8
+ * @param {Object} props.style - Custom styles for the button
9
+ * @param {Function} props.onClick - Optional callback when button is clicked
10
+ * @param {Function} props.onMouseEnter - Optional mouse enter handler
11
+ * @param {Function} props.onMouseLeave - Optional mouse leave handler
7
12
  */
8
- export default function ApiDocsButton() {
13
+ export default function ApiDocsButton({ style, onClick, onMouseEnter, onMouseLeave }) {
9
14
  const handleClick = () => {
10
15
  window.open('/api-docs', '_blank');
16
+ if (onClick) {
17
+ onClick();
18
+ }
19
+ };
20
+
21
+ const defaultStyle = {
22
+ position: 'fixed',
23
+ bottom: '20px',
24
+ right: '80px',
25
+ background: colors.bgCard,
26
+ border: `1px solid ${colors.borderLight}`,
27
+ borderRadius: '50%',
28
+ width: '48px',
29
+ height: '48px',
30
+ padding: 0,
31
+ color: colors.textSecondary,
32
+ cursor: 'pointer',
33
+ fontFamily: fonts.body,
34
+ boxShadow: `0 4px 12px ${colors.shadowMd}`,
35
+ display: 'flex',
36
+ alignItems: 'center',
37
+ justifyContent: 'center',
38
+ zIndex: 9999,
39
+ transition: 'all 0.2s ease',
40
+ ...style,
41
+ };
42
+
43
+ const handleMouseEnter = (e) => {
44
+ e.currentTarget.style.background = colors.bgHover;
45
+ e.currentTarget.style.color = colors.textPrimary;
46
+ e.currentTarget.style.borderColor = colors.borderMedium;
47
+ e.currentTarget.style.transform = 'scale(1.1)';
48
+ e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
49
+ if (onMouseEnter) {
50
+ onMouseEnter(e);
51
+ }
52
+ };
53
+
54
+ const handleMouseLeave = (e) => {
55
+ e.currentTarget.style.background = colors.bgCard;
56
+ e.currentTarget.style.color = colors.textSecondary;
57
+ e.currentTarget.style.borderColor = colors.borderLight;
58
+ e.currentTarget.style.transform = 'scale(1)';
59
+ e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
60
+ if (onMouseLeave) {
61
+ onMouseLeave(e);
62
+ }
11
63
  };
12
64
 
13
65
  return (
@@ -15,40 +67,9 @@ export default function ApiDocsButton() {
15
67
  type="button"
16
68
  onClick={handleClick}
17
69
  data-tour="api-docs-button"
18
- style={{
19
- position: 'fixed',
20
- bottom: '20px',
21
- right: '80px',
22
- background: colors.bgCard,
23
- border: `1px solid ${colors.borderLight}`,
24
- borderRadius: '50%',
25
- width: '48px',
26
- height: '48px',
27
- padding: 0,
28
- color: colors.textSecondary,
29
- cursor: 'pointer',
30
- fontFamily: fonts.body,
31
- boxShadow: `0 4px 12px ${colors.shadowMd}`,
32
- display: 'flex',
33
- alignItems: 'center',
34
- justifyContent: 'center',
35
- zIndex: 9999,
36
- transition: 'all 0.2s ease',
37
- }}
38
- onMouseEnter={(e) => {
39
- e.currentTarget.style.background = colors.bgHover;
40
- e.currentTarget.style.color = colors.textPrimary;
41
- e.currentTarget.style.borderColor = colors.borderMedium;
42
- e.currentTarget.style.transform = 'scale(1.1)';
43
- e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
44
- }}
45
- onMouseLeave={(e) => {
46
- e.currentTarget.style.background = colors.bgCard;
47
- e.currentTarget.style.color = colors.textSecondary;
48
- e.currentTarget.style.borderColor = colors.borderLight;
49
- e.currentTarget.style.transform = 'scale(1)';
50
- e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
51
- }}
70
+ style={defaultStyle}
71
+ onMouseEnter={handleMouseEnter}
72
+ onMouseLeave={handleMouseLeave}
52
73
  title="View API Documentation"
53
74
  >
54
75
  <IconApi size={20} stroke={1.5} />
@@ -20,46 +20,68 @@ const TourIcon = ({ size = 16, color = 'currentColor' }) => (
20
20
  </svg>
21
21
  );
22
22
 
23
- export default function HelpButton({ onClick }) {
23
+ /**
24
+ * Help button component
25
+ * Starts the application tour
26
+ * @param {Object} props
27
+ * @param {Function} props.onClick - Callback when button is clicked
28
+ * @param {Object} props.style - Custom styles for the button
29
+ * @param {Function} props.onMouseEnter - Optional mouse enter handler
30
+ * @param {Function} props.onMouseLeave - Optional mouse leave handler
31
+ */
32
+ export default function HelpButton({ onClick, style, onMouseEnter, onMouseLeave }) {
33
+ const defaultStyle = {
34
+ position: 'fixed',
35
+ bottom: '20px',
36
+ right: '20px',
37
+ background: colors.bgCard,
38
+ border: `1px solid ${colors.borderLight}`,
39
+ borderRadius: '50%',
40
+ width: '48px',
41
+ height: '48px',
42
+ padding: 0,
43
+ color: colors.textSecondary,
44
+ cursor: 'pointer',
45
+ fontFamily: fonts.body,
46
+ boxShadow: `0 4px 12px ${colors.shadowMd}`,
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ justifyContent: 'center',
50
+ zIndex: 9999,
51
+ transition: 'all 0.2s ease',
52
+ ...style,
53
+ };
54
+
55
+ const handleMouseEnter = (e) => {
56
+ e.currentTarget.style.background = colors.bgHover;
57
+ e.currentTarget.style.color = colors.textPrimary;
58
+ e.currentTarget.style.borderColor = colors.borderMedium;
59
+ e.currentTarget.style.transform = 'scale(1.1)';
60
+ e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
61
+ if (onMouseEnter) {
62
+ onMouseEnter(e);
63
+ }
64
+ };
65
+
66
+ const handleMouseLeave = (e) => {
67
+ e.currentTarget.style.background = colors.bgCard;
68
+ e.currentTarget.style.color = colors.textSecondary;
69
+ e.currentTarget.style.borderColor = colors.borderLight;
70
+ e.currentTarget.style.transform = 'scale(1)';
71
+ e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
72
+ if (onMouseLeave) {
73
+ onMouseLeave(e);
74
+ }
75
+ };
76
+
24
77
  return (
25
78
  <button
26
79
  type="button"
27
80
  onClick={onClick}
28
81
  data-tour="help-button"
29
- style={{
30
- position: 'fixed',
31
- bottom: '20px',
32
- right: '20px',
33
- background: colors.bgCard,
34
- border: `1px solid ${colors.borderLight}`,
35
- borderRadius: '50%',
36
- width: '48px',
37
- height: '48px',
38
- padding: 0,
39
- color: colors.textSecondary,
40
- cursor: 'pointer',
41
- fontFamily: fonts.body,
42
- boxShadow: `0 4px 12px ${colors.shadowMd}`,
43
- display: 'flex',
44
- alignItems: 'center',
45
- justifyContent: 'center',
46
- zIndex: 9999,
47
- transition: 'all 0.2s ease',
48
- }}
49
- onMouseEnter={(e) => {
50
- e.currentTarget.style.background = colors.bgHover;
51
- e.currentTarget.style.color = colors.textPrimary;
52
- e.currentTarget.style.borderColor = colors.borderMedium;
53
- e.currentTarget.style.transform = 'scale(1.1)';
54
- e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
55
- }}
56
- onMouseLeave={(e) => {
57
- e.currentTarget.style.background = colors.bgCard;
58
- e.currentTarget.style.color = colors.textSecondary;
59
- e.currentTarget.style.borderColor = colors.borderLight;
60
- e.currentTarget.style.transform = 'scale(1)';
61
- e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
62
- }}
82
+ style={defaultStyle}
83
+ onMouseEnter={handleMouseEnter}
84
+ onMouseLeave={handleMouseLeave}
63
85
  title="Start tour"
64
86
  >
65
87
  <TourIcon size={20} />
@@ -0,0 +1,142 @@
1
+ import { IconPower } from '@tabler/icons-react';
2
+ import { useState } from 'react';
3
+ import { colors, fonts } from '../../theme';
4
+ import AlertModal from '../AlertModal';
5
+ import ConfirmationModal from '../ConfirmationModal';
6
+ import ShuttingDownModal from '../ShuttingDownModal';
7
+
8
+ /**
9
+ * Shutdown button component
10
+ * Shuts down the MCP Shark application
11
+ * @param {Object} props
12
+ * @param {Object} props.style - Custom styles for the button
13
+ * @param {Function} props.onClick - Optional callback when button is clicked
14
+ * @param {Function} props.onMouseEnter - Optional mouse enter handler
15
+ * @param {Function} props.onMouseLeave - Optional mouse leave handler
16
+ */
17
+ export default function ShutdownButton({ style, onClick, onMouseEnter, onMouseLeave }) {
18
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
19
+ const [showErrorModal, setShowErrorModal] = useState(false);
20
+ const [showShuttingDownModal, setShowShuttingDownModal] = useState(false);
21
+ const [errorMessage, setErrorMessage] = useState('');
22
+
23
+ const handleShutdown = async () => {
24
+ setShowShuttingDownModal(true);
25
+
26
+ const { success, message } = await shutdownOrTimeout();
27
+ if (success) {
28
+ console.log('Shutdown successful');
29
+ window.location.reload(); // reload the page to reset the app
30
+ } else {
31
+ setShowShuttingDownModal(false);
32
+ setErrorMessage(message);
33
+ setShowErrorModal(true);
34
+ }
35
+ };
36
+
37
+ const handleClick = () => {
38
+ setShowConfirmModal(true);
39
+ if (onClick) {
40
+ onClick();
41
+ }
42
+ };
43
+
44
+ const defaultStyle = {
45
+ position: 'fixed',
46
+ bottom: '20px',
47
+ left: '20px',
48
+ background: colors.bgCard,
49
+ border: `1px solid ${colors.borderLight}`,
50
+ borderRadius: '50%',
51
+ width: '48px',
52
+ height: '48px',
53
+ padding: 0,
54
+ color: colors.textSecondary,
55
+ cursor: 'pointer',
56
+ fontFamily: fonts.body,
57
+ boxShadow: `0 4px 12px ${colors.shadowMd}`,
58
+ display: 'flex',
59
+ alignItems: 'center',
60
+ justifyContent: 'center',
61
+ zIndex: 9999,
62
+ transition: 'all 0.2s ease',
63
+ ...style,
64
+ };
65
+
66
+ const handleMouseEnter = (e) => {
67
+ e.currentTarget.style.background = colors.bgHover;
68
+ e.currentTarget.style.color = colors.error;
69
+ e.currentTarget.style.borderColor = colors.borderMedium;
70
+ e.currentTarget.style.transform = 'scale(1.1)';
71
+ e.currentTarget.style.boxShadow = `0 6px 16px ${colors.shadowLg}`;
72
+ if (onMouseEnter) {
73
+ onMouseEnter(e);
74
+ }
75
+ };
76
+
77
+ const handleMouseLeave = (e) => {
78
+ e.currentTarget.style.background = colors.bgCard;
79
+ e.currentTarget.style.color = colors.textSecondary;
80
+ e.currentTarget.style.borderColor = colors.borderLight;
81
+ e.currentTarget.style.transform = 'scale(1)';
82
+ e.currentTarget.style.boxShadow = `0 4px 12px ${colors.shadowMd}`;
83
+ if (onMouseLeave) {
84
+ onMouseLeave(e);
85
+ }
86
+ };
87
+
88
+ return (
89
+ <>
90
+ <button
91
+ type="button"
92
+ onClick={handleClick}
93
+ data-tour="shutdown-button"
94
+ style={defaultStyle}
95
+ onMouseEnter={handleMouseEnter}
96
+ onMouseLeave={handleMouseLeave}
97
+ title="Shutdown MCP Shark"
98
+ >
99
+ <IconPower size={20} stroke={1.5} />
100
+ </button>
101
+ <ConfirmationModal
102
+ isOpen={showConfirmModal}
103
+ onClose={() => setShowConfirmModal(false)}
104
+ onConfirm={handleShutdown}
105
+ title="Shutdown MCP Shark"
106
+ message="Are you sure you want to shutdown MCP Shark? This will stop the server and all services."
107
+ confirmText="Shutdown"
108
+ cancelText="Cancel"
109
+ danger={true}
110
+ />
111
+ <AlertModal
112
+ isOpen={showErrorModal}
113
+ onClose={() => setShowErrorModal(false)}
114
+ title="Shutdown Failed"
115
+ message={errorMessage}
116
+ type="error"
117
+ />
118
+ <ShuttingDownModal isOpen={showShuttingDownModal} />
119
+ </>
120
+ );
121
+ }
122
+
123
+ async function shutdownOrTimeout() {
124
+ return Promise.race([callShutdown(), timeout(3000)]);
125
+ }
126
+ async function timeout(ms) {
127
+ return new Promise((resolve) =>
128
+ setTimeout(() => resolve({ success: true, message: 'Shutdown timed out' }), ms)
129
+ );
130
+ }
131
+
132
+ async function callShutdown() {
133
+ try {
134
+ const response = await fetch('/api/composite/shutdown', {
135
+ method: 'POST',
136
+ });
137
+ return { success: response.ok, message: response.message };
138
+ } catch (error) {
139
+ console.error('Shutdown error:', error);
140
+ return { success: false, message: error.message || 'Failed to shutdown server' };
141
+ }
142
+ }