@mcp-shark/mcp-shark 1.5.0 → 1.5.3

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.
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Query functions for forensic analysis of HTTP packets
3
+ * Can be used independently in other projects
4
+ */
5
+
6
+ /**
7
+ * Query packets with forensic filters
8
+ */
9
+ export function queryPackets(db, filters = {}) {
10
+ const {
11
+ sessionId = null,
12
+ direction = null,
13
+ method = null,
14
+ jsonrpcMethod = null,
15
+ statusCode = null,
16
+ startTime = null,
17
+ endTime = null,
18
+ jsonrpcId = null,
19
+ limit = 1000,
20
+ offset = 0,
21
+ } = filters;
22
+
23
+ const queryParts = ['SELECT * FROM packets WHERE 1=1'];
24
+ const params = [];
25
+
26
+ if (sessionId) {
27
+ queryParts.push('AND session_id = ?');
28
+ params.push(sessionId);
29
+ }
30
+
31
+ if (direction) {
32
+ queryParts.push('AND direction = ?');
33
+ params.push(direction);
34
+ }
35
+
36
+ if (method) {
37
+ queryParts.push('AND method = ?');
38
+ params.push(method);
39
+ }
40
+
41
+ if (jsonrpcMethod) {
42
+ queryParts.push('AND jsonrpc_method = ?');
43
+ params.push(jsonrpcMethod);
44
+ }
45
+
46
+ if (statusCode !== null && statusCode !== undefined) {
47
+ queryParts.push('AND status_code = ?');
48
+ params.push(statusCode);
49
+ }
50
+
51
+ if (startTime) {
52
+ queryParts.push('AND timestamp_ns >= ?');
53
+ params.push(startTime);
54
+ }
55
+
56
+ if (endTime) {
57
+ queryParts.push('AND timestamp_ns <= ?');
58
+ params.push(endTime);
59
+ }
60
+
61
+ if (jsonrpcId) {
62
+ queryParts.push('AND jsonrpc_id = ?');
63
+ params.push(jsonrpcId);
64
+ }
65
+
66
+ queryParts.push('ORDER BY timestamp_ns ASC LIMIT ? OFFSET ?');
67
+ params.push(limit, offset);
68
+
69
+ const query = queryParts.join(' ');
70
+ const stmt = db.prepare(query);
71
+ return stmt.all(...params);
72
+ }
73
+
74
+ /**
75
+ * Enhanced query function for requests/responses with search and server name filtering
76
+ * Supports partial matching and general search across multiple fields
77
+ */
78
+ export function queryRequests(db, filters = {}) {
79
+ const {
80
+ sessionId = null,
81
+ direction = null,
82
+ method = null,
83
+ jsonrpcMethod = null,
84
+ statusCode = null,
85
+ startTime = null,
86
+ endTime = null,
87
+ jsonrpcId = null,
88
+ search = null, // General search across multiple fields
89
+ serverName = null, // MCP server name filter
90
+ limit = 1000,
91
+ offset = 0,
92
+ } = filters;
93
+
94
+ const queryParts = ['SELECT * FROM packets WHERE 1=1'];
95
+ const params = [];
96
+
97
+ // General search - searches across multiple fields with partial matching
98
+ // Also searches for server names in JSON-RPC params (e.g., "params":{"name":"server-name.tool")
99
+ if (search) {
100
+ const searchPattern = `%${search}%`;
101
+ // Also create a pattern to search for server name in params (e.g., "name":"server-name")
102
+ const serverNamePattern = `%"name":"${search}%`;
103
+ queryParts.push(`AND (
104
+ session_id LIKE ? ESCAPE '\\' OR
105
+ method LIKE ? ESCAPE '\\' OR
106
+ url LIKE ? ESCAPE '\\' OR
107
+ jsonrpc_method LIKE ? ESCAPE '\\' OR
108
+ jsonrpc_id LIKE ? ESCAPE '\\' OR
109
+ info LIKE ? ESCAPE '\\' OR
110
+ body_raw LIKE ? ESCAPE '\\' OR
111
+ body_json LIKE ? ESCAPE '\\' OR
112
+ headers_json LIKE ? ESCAPE '\\' OR
113
+ host LIKE ? ESCAPE '\\' OR
114
+ remote_address LIKE ? ESCAPE '\\' OR
115
+ -- Search for server name in JSON-RPC params (e.g., "params":{"name":"server-name.tool")
116
+ body_json LIKE ? ESCAPE '\\' OR
117
+ body_raw LIKE ? ESCAPE '\\'
118
+ )`);
119
+ // Add the pattern for each field (11 regular fields + 2 server name fields = 13 total)
120
+ for (let i = 0; i < 11; i++) {
121
+ params.push(searchPattern);
122
+ }
123
+ // Add server name specific patterns
124
+ params.push(serverNamePattern);
125
+ params.push(serverNamePattern);
126
+ }
127
+
128
+ // Specific field filters with partial matching
129
+ if (sessionId) {
130
+ queryParts.push("AND session_id LIKE ? ESCAPE '\\'");
131
+ params.push(`%${sessionId}%`);
132
+ }
133
+ if (direction) {
134
+ queryParts.push('AND direction = ?');
135
+ params.push(direction);
136
+ }
137
+ if (method) {
138
+ queryParts.push("AND method LIKE ? ESCAPE '\\'");
139
+ params.push(`%${method}%`);
140
+ }
141
+ if (jsonrpcMethod) {
142
+ queryParts.push("AND jsonrpc_method LIKE ? ESCAPE '\\'");
143
+ params.push(`%${jsonrpcMethod}%`);
144
+ }
145
+ if (statusCode !== null && statusCode !== undefined) {
146
+ queryParts.push('AND status_code = ?');
147
+ params.push(statusCode);
148
+ }
149
+ if (startTime) {
150
+ queryParts.push('AND timestamp_ns >= ?');
151
+ params.push(startTime);
152
+ }
153
+ if (endTime) {
154
+ queryParts.push('AND timestamp_ns <= ?');
155
+ params.push(endTime);
156
+ }
157
+ if (jsonrpcId) {
158
+ queryParts.push("AND jsonrpc_id LIKE ? ESCAPE '\\'");
159
+ params.push(`%${jsonrpcId}%`);
160
+ }
161
+
162
+ // Filter by MCP server name - search in JSON-RPC params
163
+ // Server names appear as "params":{"name":"server-name.tool-name" or "name":"server-name.tool-name"
164
+ if (serverName) {
165
+ const serverPattern = `%"name":"${serverName}.%`;
166
+ const serverPattern2 = `%"name":"${serverName}"%`;
167
+ queryParts.push(`AND (
168
+ body_json LIKE ? ESCAPE '\\' OR
169
+ body_raw LIKE ? ESCAPE '\\' OR
170
+ body_json LIKE ? ESCAPE '\\' OR
171
+ body_raw LIKE ? ESCAPE '\\'
172
+ )`);
173
+ params.push(serverPattern);
174
+ params.push(serverPattern);
175
+ params.push(serverPattern2);
176
+ params.push(serverPattern2);
177
+ }
178
+
179
+ queryParts.push('ORDER BY timestamp_ns DESC LIMIT ? OFFSET ?');
180
+ params.push(limit, offset);
181
+
182
+ const query = queryParts.join(' ');
183
+ const stmt = db.prepare(query);
184
+ return stmt.all(...params);
185
+ }
186
+
187
+ /**
188
+ * Get conversation flow (request/response pairs)
189
+ */
190
+ export function queryConversations(db, filters = {}) {
191
+ const {
192
+ sessionId = null,
193
+ method = null,
194
+ status = null,
195
+ startTime = null,
196
+ endTime = null,
197
+ jsonrpcId = null,
198
+ limit = 1000,
199
+ offset = 0,
200
+ } = filters;
201
+
202
+ const queryParts = [
203
+ 'SELECT',
204
+ ' c.*,',
205
+ ' req.frame_number as req_frame,',
206
+ ' req.timestamp_iso as req_timestamp_iso,',
207
+ ' req.method as req_method,',
208
+ ' req.url as req_url,',
209
+ ' req.jsonrpc_method as req_jsonrpc_method,',
210
+ ' req.body_json as req_body_json,',
211
+ ' req.headers_json as req_headers_json,',
212
+ ' resp.frame_number as resp_frame,',
213
+ ' resp.timestamp_iso as resp_timestamp_iso,',
214
+ ' resp.status_code as resp_status_code,',
215
+ ' resp.jsonrpc_method as resp_jsonrpc_method,',
216
+ ' resp.body_json as resp_body_json,',
217
+ ' resp.headers_json as resp_headers_json,',
218
+ ' resp.jsonrpc_result as resp_jsonrpc_result,',
219
+ ' resp.jsonrpc_error as resp_jsonrpc_error',
220
+ 'FROM conversations c',
221
+ 'LEFT JOIN packets req ON c.request_frame_number = req.frame_number',
222
+ 'LEFT JOIN packets resp ON c.response_frame_number = resp.frame_number',
223
+ 'WHERE 1=1',
224
+ ];
225
+
226
+ const params = [];
227
+
228
+ if (sessionId) {
229
+ queryParts.push('AND c.session_id = ?');
230
+ params.push(sessionId);
231
+ }
232
+
233
+ if (method) {
234
+ queryParts.push('AND c.method = ?');
235
+ params.push(method);
236
+ }
237
+
238
+ if (status) {
239
+ queryParts.push('AND c.status = ?');
240
+ params.push(status);
241
+ }
242
+
243
+ if (startTime) {
244
+ queryParts.push('AND c.request_timestamp_ns >= ?');
245
+ params.push(startTime);
246
+ }
247
+
248
+ if (endTime) {
249
+ queryParts.push('AND c.request_timestamp_ns <= ?');
250
+ params.push(endTime);
251
+ }
252
+
253
+ if (jsonrpcId) {
254
+ queryParts.push('AND c.jsonrpc_id = ?');
255
+ params.push(jsonrpcId);
256
+ }
257
+
258
+ queryParts.push('ORDER BY c.request_timestamp_ns ASC LIMIT ? OFFSET ?');
259
+ params.push(limit, offset);
260
+
261
+ const query = queryParts.join(' ');
262
+ const stmt = db.prepare(query);
263
+ return stmt.all(...params);
264
+ }
265
+
266
+ /**
267
+ * Get all packets for a specific session (for forensic analysis)
268
+ */
269
+ export function getSessionPackets(db, sessionId, limit = 10000) {
270
+ const stmt = db.prepare(`
271
+ SELECT * FROM packets
272
+ WHERE session_id = ?
273
+ ORDER BY timestamp_ns ASC
274
+ LIMIT ?
275
+ `);
276
+ return stmt.all(sessionId, limit);
277
+ }
278
+
279
+ /**
280
+ * Get all requests for a specific session (ordered by most recent first)
281
+ */
282
+ export function getSessionRequests(db, sessionId, limit = 10000) {
283
+ const stmt = db.prepare(`
284
+ SELECT * FROM packets
285
+ WHERE session_id = ?
286
+ ORDER BY timestamp_ns DESC
287
+ LIMIT ?
288
+ `);
289
+ return stmt.all(sessionId, limit);
290
+ }
291
+
292
+ /**
293
+ * Get session metadata
294
+ */
295
+ export function getSessions(db, filters = {}) {
296
+ const { startTime = null, endTime = null, limit = 1000, offset = 0 } = filters;
297
+
298
+ const queryParts = ['SELECT * FROM sessions WHERE 1=1'];
299
+ const params = [];
300
+
301
+ if (startTime) {
302
+ queryParts.push('AND first_seen_ns >= ?');
303
+ params.push(startTime);
304
+ }
305
+
306
+ if (endTime) {
307
+ queryParts.push('AND last_seen_ns <= ?');
308
+ params.push(endTime);
309
+ }
310
+
311
+ queryParts.push('ORDER BY first_seen_ns DESC LIMIT ? OFFSET ?');
312
+ params.push(limit, offset);
313
+
314
+ const query = queryParts.join(' ');
315
+ const stmt = db.prepare(query);
316
+ return stmt.all(...params);
317
+ }
318
+
319
+ /**
320
+ * Get statistics for forensic analysis
321
+ */
322
+ export function getStatistics(db, filters = {}) {
323
+ const { sessionId = null, startTime = null, endTime = null } = filters;
324
+
325
+ const whereParts = ['WHERE 1=1'];
326
+ const params = [];
327
+
328
+ if (sessionId) {
329
+ whereParts.push('AND session_id = ?');
330
+ params.push(sessionId);
331
+ }
332
+
333
+ if (startTime) {
334
+ whereParts.push('AND timestamp_ns >= ?');
335
+ params.push(startTime);
336
+ }
337
+
338
+ if (endTime) {
339
+ whereParts.push('AND timestamp_ns <= ?');
340
+ params.push(endTime);
341
+ }
342
+
343
+ const whereClause = whereParts.join(' ');
344
+ const statsQuery = `
345
+ SELECT
346
+ COUNT(*) as total_packets,
347
+ COUNT(CASE WHEN direction = 'request' THEN 1 END) as total_requests,
348
+ COUNT(CASE WHEN direction = 'response' THEN 1 END) as total_responses,
349
+ COUNT(CASE WHEN status_code >= 400 THEN 1 END) as total_errors,
350
+ COUNT(DISTINCT session_id) as unique_sessions,
351
+ AVG(length) as avg_packet_size,
352
+ SUM(length) as total_bytes,
353
+ MIN(timestamp_ns) as first_packet_ns,
354
+ MAX(timestamp_ns) as last_packet_ns
355
+ FROM packets
356
+ ${whereClause}
357
+ `;
358
+
359
+ const stmt = db.prepare(statsQuery);
360
+ return stmt.get(...params);
361
+ }
362
+
363
+ /**
364
+ * Get conversation statistics
365
+ */
366
+ export function getConversationStatistics(db, filters = {}) {
367
+ const { sessionId = null, startTime = null, endTime = null } = filters;
368
+
369
+ const whereParts = ['WHERE 1=1'];
370
+ const params = [];
371
+
372
+ if (sessionId) {
373
+ whereParts.push('AND session_id = ?');
374
+ params.push(sessionId);
375
+ }
376
+
377
+ if (startTime) {
378
+ whereParts.push('AND request_timestamp_ns >= ?');
379
+ params.push(startTime);
380
+ }
381
+
382
+ if (endTime) {
383
+ whereParts.push('AND request_timestamp_ns <= ?');
384
+ params.push(endTime);
385
+ }
386
+
387
+ const whereClause = whereParts.join(' ');
388
+ const statsQuery = `
389
+ SELECT
390
+ COUNT(*) as total_conversations,
391
+ COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
392
+ COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending,
393
+ COUNT(CASE WHEN status = 'error' THEN 1 END) as errors,
394
+ AVG(duration_ms) as avg_duration_ms,
395
+ MIN(duration_ms) as min_duration_ms,
396
+ MAX(duration_ms) as max_duration_ms
397
+ FROM conversations
398
+ ${whereClause}
399
+ `;
400
+
401
+ const stmt = db.prepare(statsQuery);
402
+ return stmt.get(...params);
403
+ }
@@ -0,0 +1,90 @@
1
+ import { consola } from 'consola';
2
+
3
+ /**
4
+ * Unified logger for the entire codebase
5
+ * Uses consola (already installed) with a consistent API
6
+ * Supports both Pino-style API (object, message) and consola-style (multiple args)
7
+ */
8
+ const logger = {
9
+ info: (...args) => {
10
+ if (args.length === 0) {
11
+ return;
12
+ }
13
+ // If first arg is an object and second is a string, use Pino-style
14
+ if (
15
+ args.length === 2 &&
16
+ typeof args[0] === 'object' &&
17
+ args[0] !== null &&
18
+ typeof args[1] === 'string'
19
+ ) {
20
+ consola.info(args[1] || '', args[0]);
21
+ } else {
22
+ // Otherwise, pass all args to consola (supports multiple strings/values)
23
+ consola.info(...args);
24
+ }
25
+ },
26
+ error: (...args) => {
27
+ if (args.length === 0) {
28
+ return;
29
+ }
30
+ if (
31
+ args.length === 2 &&
32
+ typeof args[0] === 'object' &&
33
+ args[0] !== null &&
34
+ typeof args[1] === 'string'
35
+ ) {
36
+ consola.error(args[1] || '', args[0]);
37
+ } else {
38
+ consola.error(...args);
39
+ }
40
+ },
41
+ warn: (...args) => {
42
+ if (args.length === 0) {
43
+ return;
44
+ }
45
+ if (
46
+ args.length === 2 &&
47
+ typeof args[0] === 'object' &&
48
+ args[0] !== null &&
49
+ typeof args[1] === 'string'
50
+ ) {
51
+ consola.warn(args[1] || '', args[0]);
52
+ } else {
53
+ consola.warn(...args);
54
+ }
55
+ },
56
+ debug: (...args) => {
57
+ if (args.length === 0) {
58
+ return;
59
+ }
60
+ if (
61
+ args.length === 2 &&
62
+ typeof args[0] === 'object' &&
63
+ args[0] !== null &&
64
+ typeof args[1] === 'string'
65
+ ) {
66
+ consola.debug(args[1] || '', args[0]);
67
+ } else {
68
+ consola.debug(...args);
69
+ }
70
+ },
71
+ log: (...args) => {
72
+ if (args.length === 0) {
73
+ return;
74
+ }
75
+ if (
76
+ args.length === 2 &&
77
+ typeof args[0] === 'object' &&
78
+ args[0] !== null &&
79
+ typeof args[1] === 'string'
80
+ ) {
81
+ consola.log(args[1] || '', args[0]);
82
+ } else {
83
+ consola.log(...args);
84
+ }
85
+ },
86
+ // Expose consola directly for advanced usage
87
+ consola,
88
+ };
89
+
90
+ export default logger;
@@ -1,16 +1,12 @@
1
1
  import { createServer } from 'node:http';
2
2
 
3
- import serverLogger from '../shared/logger.js';
3
+ import serverLogger from '#common/logger';
4
4
  import { isError } from './lib/common/error.js';
5
5
  import { runAllExternalServers } from './lib/server/external/all.js';
6
6
 
7
- import {
8
- getDatabaseFile,
9
- getMcpConfigPath,
10
- prepareAppDataSpaces,
11
- } from 'mcp-shark-common/configs/index.js';
12
- import { initDb } from 'mcp-shark-common/db/init.js';
13
- import { getLogger } from 'mcp-shark-common/db/logger.js';
7
+ import { getDatabaseFile, getMcpConfigPath, prepareAppDataSpaces } from '#common/configs';
8
+ import { initDb } from '#common/db/init';
9
+ import { getLogger } from '#common/db/logger';
14
10
  import { withAuditRequestResponseHandler } from './lib/auditor/audit.js';
15
11
  import { getInternalServer } from './lib/server/internal/run.js';
16
12
  import { createInternalServerFactory } from './lib/server/internal/server.js';
@@ -1,4 +1,4 @@
1
- import logger from '../shared/logger.js';
1
+ import logger from '#common/logger';
2
2
  // CLI entry point - uses the library
3
3
  import { startMcpSharkServer } from './index.js';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-shark/mcp-shark",
3
- "version": "1.5.0",
3
+ "version": "1.5.3",
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",
@@ -8,6 +8,14 @@
8
8
  ".": "./bin/mcp-shark.js",
9
9
  "./mcp-server": "./mcp-server/index.js"
10
10
  },
11
+ "imports": {
12
+ "#common/configs": "./lib/common/configs/index.js",
13
+ "#common/db/query": "./lib/common/db/query.js",
14
+ "#common/db/init": "./lib/common/db/init.js",
15
+ "#common/db/logger": "./lib/common/db/logger.js",
16
+ "#common/logger": "./lib/common/logger.js",
17
+ "#common/*": "./lib/common/*"
18
+ },
11
19
  "bin": {
12
20
  "mcp-shark": "./bin/mcp-shark.js",
13
21
  "@mcp-shark/mcp-shark": "./bin/mcp-shark.js"
@@ -20,7 +28,7 @@
20
28
  "bugs": {
21
29
  "url": "https://github.com/mcp-shark/mcp-shark/issues"
22
30
  },
23
- "files": ["bin", "ui", "mcp-server", "README.md", "LICENSE", "package.json"],
31
+ "files": ["bin", "ui", "mcp-server", "lib", "README.md", "LICENSE", "package.json"],
24
32
  "scripts": {
25
33
  "start": "node scripts/start-ui.js",
26
34
  "predev": "npm run build:ui",
@@ -61,7 +69,7 @@
61
69
  "author": "",
62
70
  "license": "SEE LICENSE IN LICENSE",
63
71
  "engines": {
64
- "node": ">=18.0.0"
72
+ "node": ">=20.0.0"
65
73
  },
66
74
  "dependencies": {
67
75
  "@modelcontextprotocol/sdk": "^1.20.2",
@@ -72,7 +80,7 @@
72
80
  "cors": "^2.8.5",
73
81
  "express": "^4.18.2",
74
82
  "jsonrpc-lite": "^2.2.0",
75
- "mcp-shark-common": "github:mcp-shark/mcp-shark-common",
83
+ "better-sqlite3": "^12.4.1",
76
84
  "open": "^11.0.0",
77
85
  "react": "^18.2.0",
78
86
  "react-dom": "^18.2.0",