@mcp-shark/mcp-shark 1.5.8 → 1.5.10

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 CHANGED
@@ -54,6 +54,7 @@ MCP Shark is a monitoring and aggregation solution for Model Context Protocol (M
54
54
  - **Security analysis**: AI-powered scanning for security risks and vulnerabilities
55
55
  - **IDE integration**: Automatic configuration detection for Cursor, Windsurf, Codex, and other IDEs
56
56
  - **API documentation**: Comprehensive Swagger/OpenAPI documentation for all endpoints with interactive testing
57
+ - **Action menu**: Expandable menu providing quick access to API docs, help tour, and server shutdown
57
58
 
58
59
  ## Documentation
59
60
 
@@ -72,7 +73,7 @@ MCP Shark is a monitoring and aggregation solution for Model Context Protocol (M
72
73
  - **[Architecture](docs/architecture.md)** - System architecture and design
73
74
  - **[Database Architecture](docs/database-architecture.md)** - Database architecture and repository pattern
74
75
  - **[API Reference](docs/api-reference.md)** - API endpoints and WebSocket protocol
75
- - **API Documentation** - Interactive Swagger/OpenAPI documentation available at `/api-docs` when server is running (or click the 📡 button in the UI)
76
+ - **API Documentation** - Interactive Swagger/OpenAPI documentation available at `/api-docs` when server is running (or click the menu button in the bottom-right corner, then select the API docs button 📡)
76
77
 
77
78
  ### Architecture & Coding Rules
78
79
  - **[Architecture Rules](rules/ARCHITECTURE_RULES.md)** - Architecture principles and guidelines
@@ -111,4 +112,4 @@ See the [LICENSE](LICENSE) file for full terms and conditions.
111
112
 
112
113
  ---
113
114
 
114
- **Version**: 1.5.4 | **Homepage**: [https://mcpshark.sh](https://mcpshark.sh)
115
+ **Version**: 1.5.9 | **Homepage**: [https://mcpshark.sh](https://mcpshark.sh)
@@ -7,14 +7,9 @@ import { Server } from '#core/constants/Server.js';
7
7
  * Provides validated access to environment variables with defaults
8
8
  */
9
9
  export const Environment = {
10
- /**
11
- * Get Codex home directory
12
- * @returns {string} Codex home path
13
- */
14
- getCodexHome() {
15
- return process.env.CODEX_HOME || join(homedir(), '.codex');
10
+ getEnv() {
11
+ return process.env;
16
12
  },
17
-
18
13
  /**
19
14
  * Get UI server port
20
15
  * @returns {number} UI server port (default: 9853)
@@ -48,4 +43,12 @@ export const Environment = {
48
43
  getMcpSharkHome() {
49
44
  return process.env.MCP_SHARK_HOME || join(homedir(), '.mcp-shark');
50
45
  },
46
+
47
+ getUserProfile() {
48
+ return process.env.USERPROFILE || homedir();
49
+ },
50
+
51
+ getCodexHome() {
52
+ return process.env.CODEX_HOME || join(homedir(), '.codex');
53
+ },
51
54
  };
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
 
5
- export * from './codex.js';
6
5
  export { Environment } from './environment.js';
7
6
 
8
7
  const WORKING_DIRECTORY_NAME = '.mcp-shark';
@@ -12,4 +12,6 @@ export const Defaults = {
12
12
 
13
13
  // Statistics defaults
14
14
  STATISTICS_LIMIT: 1000000,
15
+
16
+ DEFAULT_MCP_SERVER_PORT: 9851,
15
17
  };
@@ -2,6 +2,8 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
2
2
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
3
  import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
4
4
 
5
+ import { Environment } from '#core/configs/environment.js';
6
+
5
7
  /**
6
8
  * Library for MCP transport creation utilities
7
9
  * Pure utility - no dependencies on services or repositories
@@ -31,7 +33,7 @@ export function createTransport(serverConfig, serverName = null) {
31
33
  } = serverConfig;
32
34
 
33
35
  const env = {
34
- ...process.env,
36
+ ...Environment.getEnv(),
35
37
  ...configEnv,
36
38
  };
37
39
 
@@ -226,14 +226,13 @@ export async function withAuditRequestResponseHandler(
226
226
 
227
227
  // Extract response body as string
228
228
  const resBodyStr = resBuf.toString('utf8');
229
- const resBodyJson = parseJsonSafely(resBodyStr);
230
229
 
231
230
  const jsonrpcId = reqJsonRpc?.payload?.id !== undefined ? String(reqJsonRpc.payload.id) : null;
232
231
 
233
232
  auditLogger.logResponsePacket({
234
233
  statusCode: wrappedRes.statusCode || 200,
235
234
  headers: resHeaders,
236
- body: resBodyJson || resBodyStr,
235
+ body: resBodyStr,
237
236
  requestFrameNumber: requestResult?.frameNumber || null,
238
237
  requestTimestampNs: requestResult?.timestampNs || null,
239
238
  jsonrpcId,
@@ -11,6 +11,7 @@ import {
11
11
  getMcpConfigPath,
12
12
  prepareAppDataSpaces,
13
13
  } from '#core/configs/index.js';
14
+ import { Defaults } from '#core/constants/Defaults.js';
14
15
  import { initDb } from '#core/db/init.js';
15
16
  import { withAuditRequestResponseHandler } from './auditor/audit.js';
16
17
  import { getInternalServer } from './server/internal/run.js';
@@ -114,25 +115,27 @@ function createServerPromise(httpServer, port, serverLogger, onError, onReady) {
114
115
  * @param {number} [options.port=9851] - Port to listen on
115
116
  * @param {Function} [options.onError] - Error callback
116
117
  * @param {Function} [options.onReady] - Ready callback
118
+ * @param {Function} [options.onLog] - Log callback: (type: string, message: string) => void
117
119
  * @param {Object} options.auditLogger - Required audit logger instance (use initAuditLogger() to create)
118
120
  * @returns {Promise<{app: Express, server: http.Server, stop: Function}>} Server instance
119
121
  */
120
122
  export async function startMcpSharkServer(options = {}) {
121
123
  const {
122
124
  configPath = getMcpConfigPath(),
123
- port = 9851,
125
+ port = Defaults.DEFAULT_MCP_SERVER_PORT,
124
126
  onError,
125
127
  onReady,
128
+ onLog,
126
129
  auditLogger: providedAuditLogger,
127
130
  } = options;
128
131
 
129
132
  prepareAppDataSpaces();
130
133
 
131
- serverLogger.info('[MCP-Shark] Starting MCP server...');
132
- serverLogger.info(`[MCP-Shark] Config path: ${configPath}`);
133
- serverLogger.info(`[MCP-Shark] Database path: ${getDatabaseFile()}`);
134
- serverLogger.info(`[MCP-Shark] Working directory: ${process.cwd()}`);
135
- serverLogger.info({ path: Environment.getPath() }, '[MCP-Shark] PATH');
134
+ logServerInfo(serverLogger, onLog, 'Starting MCP server...', { port });
135
+ logServerInfo(serverLogger, onLog, 'Config path', { path: configPath });
136
+ logServerInfo(serverLogger, onLog, 'Database path', { path: getDatabaseFile() });
137
+ logServerInfo(serverLogger, onLog, 'Working directory', { path: process.cwd() });
138
+ logServerInfo(serverLogger, onLog, 'PATH', { path: Environment.getPath() });
136
139
 
137
140
  try {
138
141
  if (!providedAuditLogger) {
@@ -190,3 +193,16 @@ export async function startMcpSharkServer(options = {}) {
190
193
  throw error;
191
194
  }
192
195
  }
196
+
197
+ function logServerInfo(serverLogger, onLog, message, metadata) {
198
+ const finalMessage = `[MCP Server] ${message}`;
199
+ serverLogger.info({ ...metadata, message: finalMessage }, finalMessage);
200
+ if (onLog) {
201
+ onLog(
202
+ 'stdout',
203
+ `${finalMessage} ${Object.entries(metadata)
204
+ .map(([key, value]) => `${key}=${value}`)
205
+ .join(' ')}`
206
+ );
207
+ }
208
+ }
@@ -2,8 +2,8 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
2
2
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
3
  import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
4
4
 
5
+ import { Environment } from '#core/configs/environment.js';
5
6
  import { CompositeError } from '#core/libraries/ErrorLibrary.js';
6
-
7
7
  export class TransportError extends CompositeError {
8
8
  constructor(message, error) {
9
9
  super('TransportError', message, error);
@@ -20,7 +20,7 @@ export function makeTransport({
20
20
  }) {
21
21
  // Start with enhanced PATH
22
22
  const env = {
23
- ...process.env,
23
+ ...Environment.getEnv(),
24
24
  ...configEnv,
25
25
  };
26
26
 
@@ -69,17 +69,68 @@ export class AuditRepository {
69
69
  return `${statusCode}${rpcInfo}`;
70
70
  }
71
71
 
72
+ _isCharacterCodeObject(obj) {
73
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
74
+ return false;
75
+ }
76
+
77
+ const keys = Object.keys(obj);
78
+ if (keys.length === 0) {
79
+ return false;
80
+ }
81
+
82
+ // Check if all keys are numeric strings starting from "0"
83
+ for (let i = 0; i < keys.length; i++) {
84
+ if (keys[i] !== String(i)) {
85
+ return false;
86
+ }
87
+ // Check if values are numbers (character codes)
88
+ if (typeof obj[keys[i]] !== 'number') {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ return true;
94
+ }
95
+
96
+ _convertCharacterCodeObjectToString(obj) {
97
+ const chars = [];
98
+ const keys = Object.keys(obj).sort((a, b) => Number(a) - Number(b));
99
+ for (const key of keys) {
100
+ chars.push(String.fromCharCode(obj[key]));
101
+ }
102
+ return chars.join('');
103
+ }
104
+
72
105
  _normalizeBody(body) {
73
106
  if (!body) {
74
107
  return { bodyRaw: '', bodyJson: null };
75
108
  }
109
+
110
+ // Handle String objects (created with new String())
111
+ if (Object.prototype.toString.call(body) === '[object String]') {
112
+ const str = String(body);
113
+ return { bodyRaw: str, bodyJson: str };
114
+ }
115
+
116
+ // Handle primitive strings
76
117
  if (typeof body === 'string') {
77
118
  return { bodyRaw: body, bodyJson: body };
78
119
  }
120
+
121
+ // Handle objects
79
122
  if (typeof body === 'object') {
123
+ // Check if it's a character code object and convert it back to string
124
+ if (this._isCharacterCodeObject(body)) {
125
+ const str = this._convertCharacterCodeObjectToString(body);
126
+ return { bodyRaw: str, bodyJson: str };
127
+ }
128
+
129
+ // Otherwise, stringify normally
80
130
  const raw = JSON.stringify(body);
81
131
  return { bodyRaw: raw, bodyJson: raw };
82
132
  }
133
+
83
134
  return { bodyRaw: '', bodyJson: null };
84
135
  }
85
136
 
@@ -38,7 +38,7 @@ export class StatisticsRepository {
38
38
  COUNT(*) as total_packets,
39
39
  COUNT(CASE WHEN direction = 'request' THEN 1 END) as total_requests,
40
40
  COUNT(CASE WHEN direction = 'response' THEN 1 END) as total_responses,
41
- COUNT(CASE WHEN status_code >= ${StatusCodeRanges.CLIENT_ERROR_START} THEN 1 END) as total_errors,
41
+ COUNT(CASE WHEN status_code >= ${StatusCodeRanges.CLIENT_ERROR_MIN} THEN 1 END) as total_errors,
42
42
  COUNT(DISTINCT session_id) as unique_sessions,
43
43
  AVG(length) as avg_packet_size,
44
44
  SUM(length) as total_bytes,
@@ -1,7 +1,11 @@
1
1
  import * as fs from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import * as path from 'node:path';
4
- import { getCodexConfigPath } from '#core/configs/index.js';
4
+ import { Environment } from '#core/configs/environment.js';
5
+
6
+ const cursorDefaultPath = path.join(homedir(), '.cursor', 'mcp.json');
7
+ const windsurfDefaultPath = path.join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
8
+ const codexDefaultPath = path.join(homedir(), '.codex', 'config.toml');
5
9
 
6
10
  /**
7
11
  * Service for detecting configuration files on the system
@@ -16,16 +20,24 @@ export class ConfigDetectionService {
16
20
  const platform = process.platform;
17
21
 
18
22
  const cursorPaths = [
19
- path.join(homeDir, '.cursor', 'mcp.json'),
23
+ cursorDefaultPath,
20
24
  ...(platform === 'win32'
21
- ? [path.join(process.env.USERPROFILE || '', '.cursor', 'mcp.json')]
25
+ ? [path.join(Environment.getUserProfile(), '.cursor', 'mcp.json')]
22
26
  : []),
23
27
  ];
24
28
 
25
29
  const windsurfPaths = [
26
- path.join(homeDir, '.codeium', 'windsurf', 'mcp_config.json'),
30
+ windsurfDefaultPath,
31
+ ...(platform === 'win32'
32
+ ? [path.join(Environment.getUserProfile(), '.codeium', 'windsurf', 'mcp_config.json')]
33
+ : []),
34
+ ];
35
+
36
+ const codexPaths = [
37
+ codexDefaultPath,
38
+ path.join(Environment.getCodexHome(), 'config.toml'),
27
39
  ...(platform === 'win32'
28
- ? [path.join(process.env.USERPROFILE || '', '.codeium', 'windsurf', 'mcp_config.json')]
40
+ ? [path.join(Environment.getUserProfile(), '.codex', 'config.toml')]
29
41
  : []),
30
42
  ];
31
43
 
@@ -53,34 +65,35 @@ export class ConfigDetectionService {
53
65
  }
54
66
  }
55
67
 
56
- const codexPath = getCodexConfigPath();
57
- if (fs.existsSync(codexPath)) {
58
- detected.push({
59
- editor: 'Codex',
60
- path: codexPath,
61
- displayPath: codexPath.replace(homeDir, '~'),
62
- exists: true,
63
- });
68
+ for (const codexPath of codexPaths) {
69
+ if (fs.existsSync(codexPath)) {
70
+ detected.push({
71
+ editor: 'Codex',
72
+ path: codexPath,
73
+ displayPath: codexPath.replace(homeDir, '~'),
74
+ exists: true,
75
+ });
76
+ }
64
77
  }
65
78
 
66
79
  const defaultPaths = [
67
80
  {
68
81
  editor: 'Cursor',
69
- path: path.join(homeDir, '.cursor', 'mcp.json'),
70
- displayPath: '~/.cursor/mcp.json',
71
- exists: fs.existsSync(path.join(homeDir, '.cursor', 'mcp.json')),
82
+ path: cursorDefaultPath.replace(homeDir, '~'),
83
+ displayPath: cursorDefaultPath.replace(homeDir, '~'),
84
+ exists: fs.existsSync(cursorDefaultPath),
72
85
  },
73
86
  {
74
87
  editor: 'Windsurf',
75
- path: path.join(homeDir, '.codeium', 'windsurf', 'mcp_config.json'),
76
- displayPath: '~/.codeium/windsurf/mcp_config.json',
77
- exists: fs.existsSync(path.join(homeDir, '.codeium', 'windsurf', 'mcp_config.json')),
88
+ path: windsurfDefaultPath.replace(homeDir, '~'),
89
+ displayPath: windsurfDefaultPath.replace(homeDir, '~'),
90
+ exists: fs.existsSync(windsurfDefaultPath),
78
91
  },
79
92
  {
80
93
  editor: 'Codex',
81
- path: codexPath,
82
- displayPath: codexPath.replace(homeDir, '~'),
83
- exists: fs.existsSync(codexPath),
94
+ path: codexDefaultPath.replace(homeDir, '~'),
95
+ displayPath: codexDefaultPath.replace(homeDir, '~'),
96
+ exists: fs.existsSync(codexDefaultPath),
84
97
  },
85
98
  ];
86
99
 
@@ -1,3 +1,4 @@
1
+ import { Defaults } from '#core/constants/Defaults.js';
1
2
  import { initAuditLogger, startMcpSharkServer } from '#core/mcp-server/index.js';
2
3
 
3
4
  /**
@@ -19,13 +20,21 @@ export class ServerManagementService {
19
20
  * @param {string} [options.filePath] - Path to config file
20
21
  * @param {string} [options.fileContent] - Config file content
21
22
  * @param {Array} [options.selectedServices] - Selected services to include
22
- * @param {number} [options.port=9851] - Server port
23
+ * @param {number} [options.port=Defaults.DEFAULT_MCP_SERVER_PORT] - Server port
23
24
  * @param {Function} [options.onError] - Error callback
24
25
  * @param {Function} [options.onReady] - Ready callback
25
26
  * @returns {Promise<Object>} Setup result with convertedConfig, updatedConfig, filePath
26
27
  */
27
28
  async setup(options = {}) {
28
- const { filePath, fileContent, selectedServices, port = 9851, onError, onReady } = options;
29
+ const {
30
+ filePath,
31
+ fileContent,
32
+ selectedServices,
33
+ port = Defaults.DEFAULT_MCP_SERVER_PORT,
34
+ onError,
35
+ onReady,
36
+ onLog,
37
+ } = options;
29
38
 
30
39
  if (!filePath && !fileContent) {
31
40
  return {
@@ -63,6 +72,7 @@ export class ServerManagementService {
63
72
  await this.startServer({
64
73
  configPath: mcpsJsonPath,
65
74
  port,
75
+ onLog,
66
76
  onError: (err) => {
67
77
  if (onError) {
68
78
  onError(err);
@@ -98,7 +108,13 @@ export class ServerManagementService {
98
108
  * Start MCP Shark server
99
109
  */
100
110
  async startServer(options = {}) {
101
- const { configPath, port = 9851, onError, onReady } = options;
111
+ const {
112
+ configPath,
113
+ port = Defaults.DEFAULT_MCP_SERVER_PORT,
114
+ onError,
115
+ onReady,
116
+ onLog,
117
+ } = options;
102
118
 
103
119
  const mcpsJsonPath = configPath || this.configService.getMcpConfigPath();
104
120
 
@@ -114,8 +130,13 @@ export class ServerManagementService {
114
130
  configPath: mcpsJsonPath,
115
131
  port,
116
132
  auditLogger,
133
+ logger: this.logger,
134
+ onLog,
117
135
  onError: (err) => {
118
- this.logger?.error({ error: err.message }, 'Failed to start mcp-shark server');
136
+ this.logger?.error(
137
+ { message: err.message, stack: err.stack, error: err },
138
+ 'Failed to start mcp-shark server'
139
+ );
119
140
  this.serverInstance = null;
120
141
  if (onError) {
121
142
  onError(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-shark/mcp-shark",
3
- "version": "1.5.8",
3
+ "version": "1.5.10",
4
4
  "description": "Aggregate multiple Model Context Protocol (MCP) servers into a single unified interface with a powerful monitoring UI. Prov deep visibility into every request and response.",
5
5
  "type": "module",
6
6
  "main": "./bin/mcp-shark.js",
@@ -25,13 +25,26 @@
25
25
  "bugs": {
26
26
  "url": "https://github.com/mcp-shark/mcp-shark/issues"
27
27
  },
28
- "files": ["bin", "ui", "core", "README.md", "LICENSE", "package.json"],
28
+ "files": [
29
+ "bin",
30
+ "ui",
31
+ "core",
32
+ "!**/__tests__",
33
+ "!**/*.test.js",
34
+ "!**/*.spec.js",
35
+ "README.md",
36
+ "LICENSE",
37
+ "package.json"
38
+ ],
29
39
  "scripts": {
30
40
  "start": "node scripts/start-ui.js",
31
41
  "predev": "npm run build:ui",
32
42
  "dev": "node scripts/start-ui.js",
33
43
  "build": "bash -c 'cd ui && vite build'",
34
44
  "build:ui": "bash -c 'cd ui && vite build'",
45
+ "test": "c8 node --test --test-name-pattern='.*' --test-reporter spec $(find core ui/server -path '*/__tests__/*.test.js' -type f)",
46
+ "test:ui": "vitest run --coverage",
47
+ "test:all": "npm run test:coverage && npm run test:ui",
35
48
  "lint": "biome lint .",
36
49
  "lint:fix": "biome lint --write .",
37
50
  "format": "biome format --write .",
@@ -46,7 +59,7 @@
46
59
  "publish:dry-run": "npm publish --dry-run"
47
60
  },
48
61
  "lint-staged": {
49
- "*": ["biome check --write"]
62
+ "*.{js,jsx,ts,tsx,json}": ["biome check --write"]
50
63
  },
51
64
  "keywords": [
52
65
  "mcp",
@@ -93,9 +106,21 @@
93
106
  "@biomejs/biome": "^1.9.4",
94
107
  "@commitlint/cli": "^19.5.0",
95
108
  "@commitlint/config-conventional": "^19.5.0",
109
+ "@testing-library/jest-dom": "^6.4.0",
110
+ "@testing-library/react": "^16.0.0",
96
111
  "@vitejs/plugin-react": "^4.2.1",
112
+ "@vitest/coverage-v8": "^4.0.18",
113
+ "c8": "^10.1.2",
97
114
  "husky": "^9.1.6",
115
+ "jsdom": "^25.0.0",
98
116
  "lint-staged": "^15.2.10",
99
- "vite": "^7.3.0"
117
+ "vite": "^7.3.0",
118
+ "vitest": "^4.0.18"
119
+ },
120
+ "c8": {
121
+ "include": ["core/**/*.js", "ui/server/**/*.js"],
122
+ "exclude": ["**/__tests__/**", "**/*.test.js", "**/index.js"],
123
+ "reporter": ["text", "html"],
124
+ "check-coverage": false
100
125
  }
101
126
  }