@mcp-shark/mcp-shark 1.5.2 → 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.
- package/bin/mcp-shark.js +1 -53
- package/lib/common/configs/index.js +108 -0
- package/lib/common/db/init.js +132 -0
- package/lib/common/db/logger.js +349 -0
- package/lib/common/db/query.js +403 -0
- package/mcp-server/index.js +4 -8
- package/mcp-server/mcp-shark.js +1 -1
- package/package.json +12 -4
- package/ui/dist/assets/{index-srLDlk97.js → index-CFHeMNwd.js} +7 -7
- package/ui/dist/index.html +1 -1
- package/ui/server/routes/composite/servers.js +1 -1
- package/ui/server/routes/composite/setup.js +1 -1
- package/ui/server/routes/conversations.js +1 -1
- package/ui/server/routes/help.js +1 -1
- package/ui/server/routes/requests.js +2 -2
- package/ui/server/routes/sessions.js +1 -1
- package/ui/server/routes/settings.js +1 -1
- package/ui/server/routes/smartscan/discover.js +1 -1
- package/ui/server/routes/statistics.js +1 -1
- package/ui/server/utils/logger.js +2 -2
- package/ui/server/utils/scan-cache/directory.js +1 -1
- package/ui/server/utils/smartscan-token.js +1 -1
- package/ui/server.js +3 -3
- package/ui/src/components/SmartScan/SmartScanControls.jsx +1 -0
- /package/{shared → lib/common}/logger.js +0 -0
package/bin/mcp-shark.js
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import open from 'open';
|
|
9
|
-
import logger from '
|
|
9
|
+
import logger from '#common/logger';
|
|
10
10
|
|
|
11
11
|
const SERVER_URL = 'http://localhost:9853';
|
|
12
12
|
const BROWSER_OPEN_DELAY = 1000;
|
|
@@ -40,46 +40,6 @@ function displayWelcomeBanner() {
|
|
|
40
40
|
|
|
41
41
|
const uiDir = join(rootDir, 'ui');
|
|
42
42
|
const distDir = join(uiDir, 'dist');
|
|
43
|
-
const rootNodeModules = join(rootDir, 'node_modules');
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Run a command and return a promise that resolves when it completes
|
|
47
|
-
*/
|
|
48
|
-
function runCommand(command, args, options) {
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
const process = spawn(command, args, {
|
|
51
|
-
...options,
|
|
52
|
-
stdio: 'inherit',
|
|
53
|
-
shell: true,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
process.on('close', (code) => {
|
|
57
|
-
if (code !== 0) {
|
|
58
|
-
reject(new Error(`Command failed with exit code ${code}`));
|
|
59
|
-
} else {
|
|
60
|
-
resolve();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
process.on('error', (error) => {
|
|
65
|
-
reject(error);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Install dependencies in the root directory
|
|
72
|
-
*/
|
|
73
|
-
async function installDependencies() {
|
|
74
|
-
logger.info('Installing dependencies...');
|
|
75
|
-
try {
|
|
76
|
-
await runCommand('npm', ['install'], { cwd: rootDir });
|
|
77
|
-
logger.info('Dependencies installed successfully!\n');
|
|
78
|
-
} catch (error) {
|
|
79
|
-
logger.error({ error: error.message }, 'Failed to install dependencies');
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
43
|
|
|
84
44
|
/**
|
|
85
45
|
* Validate that UI dist directory exists
|
|
@@ -172,15 +132,6 @@ function validateDirectories() {
|
|
|
172
132
|
}
|
|
173
133
|
}
|
|
174
134
|
|
|
175
|
-
/**
|
|
176
|
-
* Ensure dependencies are installed
|
|
177
|
-
*/
|
|
178
|
-
async function ensureDependencies() {
|
|
179
|
-
if (!existsSync(rootNodeModules)) {
|
|
180
|
-
await installDependencies();
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
135
|
/**
|
|
185
136
|
* Main execution function
|
|
186
137
|
*/
|
|
@@ -197,9 +148,6 @@ async function main() {
|
|
|
197
148
|
// Validate environment
|
|
198
149
|
validateDirectories();
|
|
199
150
|
|
|
200
|
-
// Ensure dependencies are installed
|
|
201
|
-
await ensureDependencies();
|
|
202
|
-
|
|
203
151
|
// Validate UI is built (pre-built files should be included in package)
|
|
204
152
|
validateUIBuilt();
|
|
205
153
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const WORKING_DIRECTORY_NAME = '.mcp-shark';
|
|
6
|
+
const MCP_CONFIG_NAME = 'mcps.json';
|
|
7
|
+
const APP_DB_DIR_NAME = 'db';
|
|
8
|
+
const APP_DB_FILE_NAME = 'mcp-shark.sqlite';
|
|
9
|
+
const HELP_STATE_NAME = 'help-state.json';
|
|
10
|
+
|
|
11
|
+
export function getWorkingDirectory() {
|
|
12
|
+
return join(homedir(), WORKING_DIRECTORY_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getDatabasePath() {
|
|
16
|
+
return join(getWorkingDirectory(), APP_DB_DIR_NAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getDatabaseFile() {
|
|
20
|
+
return join(getDatabasePath(), APP_DB_FILE_NAME);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createWorkingDirectorySpaces() {
|
|
24
|
+
const workingDirectory = getWorkingDirectory();
|
|
25
|
+
if (!existsSync(workingDirectory)) {
|
|
26
|
+
mkdirSync(workingDirectory, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createDatabaseSpaces() {
|
|
31
|
+
createWorkingDirectorySpaces();
|
|
32
|
+
const databasePath = getDatabasePath();
|
|
33
|
+
if (!existsSync(databasePath)) {
|
|
34
|
+
mkdirSync(databasePath, { recursive: true });
|
|
35
|
+
const databaseFile = getDatabaseFile();
|
|
36
|
+
if (!existsSync(databaseFile)) {
|
|
37
|
+
writeFileSync(databaseFile, '');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getMcpConfigPath() {
|
|
43
|
+
return join(getWorkingDirectory(), MCP_CONFIG_NAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function prepareAppDataSpaces() {
|
|
47
|
+
createWorkingDirectorySpaces();
|
|
48
|
+
createDatabaseSpaces();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getHelpStatePath() {
|
|
52
|
+
return join(getWorkingDirectory(), HELP_STATE_NAME);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readHelpState() {
|
|
56
|
+
try {
|
|
57
|
+
const helpStatePath = getHelpStatePath();
|
|
58
|
+
if (existsSync(helpStatePath)) {
|
|
59
|
+
const content = readFileSync(helpStatePath, 'utf8');
|
|
60
|
+
const state = JSON.parse(content);
|
|
61
|
+
// Ensure we have the expected structure
|
|
62
|
+
return {
|
|
63
|
+
dismissed: state.dismissed || false,
|
|
64
|
+
tourCompleted: state.tourCompleted || false,
|
|
65
|
+
dismissedAt: state.dismissedAt || null,
|
|
66
|
+
version: state.version || '1.0.0',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
dismissed: false,
|
|
71
|
+
tourCompleted: false,
|
|
72
|
+
dismissedAt: null,
|
|
73
|
+
version: '1.0.0',
|
|
74
|
+
};
|
|
75
|
+
} catch (_error) {
|
|
76
|
+
// Error reading help state - return defaults
|
|
77
|
+
return {
|
|
78
|
+
dismissed: false,
|
|
79
|
+
tourCompleted: false,
|
|
80
|
+
dismissedAt: null,
|
|
81
|
+
version: '1.0.0',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function writeHelpState(state) {
|
|
87
|
+
try {
|
|
88
|
+
const helpStatePath = getHelpStatePath();
|
|
89
|
+
prepareAppDataSpaces(); // Ensure directory exists
|
|
90
|
+
|
|
91
|
+
// Merge with existing state to preserve other fields
|
|
92
|
+
const existingState = readHelpState();
|
|
93
|
+
const newState = {
|
|
94
|
+
...existingState,
|
|
95
|
+
...state,
|
|
96
|
+
dismissedAt:
|
|
97
|
+
state.dismissed || state.tourCompleted
|
|
98
|
+
? new Date().toISOString()
|
|
99
|
+
: existingState.dismissedAt,
|
|
100
|
+
version: '1.0.0',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
writeFileSync(helpStatePath, JSON.stringify(newState, null, 2));
|
|
104
|
+
return true;
|
|
105
|
+
} catch (_error) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
|
|
5
|
+
function createTables(db) {
|
|
6
|
+
db.exec(`
|
|
7
|
+
-- Packet capture table
|
|
8
|
+
-- Each HTTP request/response is stored as a packet for forensic analysis
|
|
9
|
+
CREATE TABLE IF NOT EXISTS packets (
|
|
10
|
+
frame_number INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
|
|
12
|
+
-- Timestamps (nanosecond precision)
|
|
13
|
+
timestamp_ns INTEGER NOT NULL, -- Unix timestamp in nanoseconds
|
|
14
|
+
timestamp_iso TEXT NOT NULL, -- ISO 8601 formatted timestamp for readability
|
|
15
|
+
|
|
16
|
+
-- Packet direction and protocol
|
|
17
|
+
direction TEXT NOT NULL CHECK(direction IN ('request', 'response')),
|
|
18
|
+
protocol TEXT NOT NULL DEFAULT 'HTTP',
|
|
19
|
+
|
|
20
|
+
-- Session identification (normalized from various header formats)
|
|
21
|
+
session_id TEXT, -- Normalized session ID (from mcp-session-id, Mcp-Session-Id, or X-MCP-Session-Id)
|
|
22
|
+
|
|
23
|
+
-- HTTP metadata
|
|
24
|
+
method TEXT, -- HTTP method (GET, POST, etc.)
|
|
25
|
+
url TEXT, -- Request URL/path
|
|
26
|
+
status_code INTEGER, -- HTTP status code (for responses)
|
|
27
|
+
|
|
28
|
+
-- Headers and body
|
|
29
|
+
headers_json TEXT NOT NULL, -- Full HTTP headers as JSON
|
|
30
|
+
body_raw TEXT, -- Raw body content
|
|
31
|
+
body_json TEXT, -- Parsed JSON body (if applicable)
|
|
32
|
+
|
|
33
|
+
-- JSON-RPC metadata (for correlation)
|
|
34
|
+
jsonrpc_id TEXT, -- JSON-RPC request ID
|
|
35
|
+
jsonrpc_method TEXT, -- JSON-RPC method (e.g., 'tools/list', 'tools/call')
|
|
36
|
+
jsonrpc_result TEXT, -- JSON-RPC result (for responses, as JSON string)
|
|
37
|
+
jsonrpc_error TEXT, -- JSON-RPC error (for error responses, as JSON string)
|
|
38
|
+
|
|
39
|
+
-- Packet metadata
|
|
40
|
+
length INTEGER NOT NULL, -- Total packet size in bytes
|
|
41
|
+
info TEXT, -- Summary info for quick viewing
|
|
42
|
+
|
|
43
|
+
-- Network metadata
|
|
44
|
+
user_agent TEXT, -- User agent string
|
|
45
|
+
remote_address TEXT, -- Remote IP address
|
|
46
|
+
host TEXT -- Host header value
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
-- Conversations table - correlates request/response pairs
|
|
50
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
51
|
+
conversation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
request_frame_number INTEGER NOT NULL,
|
|
53
|
+
response_frame_number INTEGER,
|
|
54
|
+
session_id TEXT,
|
|
55
|
+
jsonrpc_id TEXT,
|
|
56
|
+
method TEXT,
|
|
57
|
+
request_timestamp_ns INTEGER NOT NULL,
|
|
58
|
+
response_timestamp_ns INTEGER,
|
|
59
|
+
duration_ms REAL, -- Round-trip time in milliseconds
|
|
60
|
+
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'completed', 'timeout', 'error')),
|
|
61
|
+
|
|
62
|
+
FOREIGN KEY (request_frame_number) REFERENCES packets(frame_number),
|
|
63
|
+
FOREIGN KEY (response_frame_number) REFERENCES packets(frame_number)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Sessions table - tracks session metadata
|
|
67
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
68
|
+
session_id TEXT PRIMARY KEY,
|
|
69
|
+
first_seen_ns INTEGER NOT NULL,
|
|
70
|
+
last_seen_ns INTEGER NOT NULL,
|
|
71
|
+
packet_count INTEGER DEFAULT 0,
|
|
72
|
+
user_agent TEXT,
|
|
73
|
+
remote_address TEXT,
|
|
74
|
+
host TEXT
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
-- Create indexes for forensic analysis
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_packets_timestamp ON packets(timestamp_ns);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_packets_session ON packets(session_id);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_packets_direction ON packets(direction);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_packets_jsonrpc_id ON packets(jsonrpc_id);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_packets_jsonrpc_method ON packets(jsonrpc_method);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_packets_method ON packets(method);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_packets_status_code ON packets(status_code);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_packets_session_timestamp ON packets(session_id, timestamp_ns);
|
|
86
|
+
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_session ON conversations(session_id);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_jsonrpc_id ON conversations(jsonrpc_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_request_frame ON conversations(request_frame_number);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_response_frame ON conversations(response_frame_number);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_timestamp ON conversations(request_timestamp_ns);
|
|
92
|
+
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_first_seen ON sessions(first_seen_ns);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_last_seen ON sessions(last_seen_ns);
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
return db;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function initDb(dbConnectionString) {
|
|
101
|
+
const db = new Database(dbConnectionString);
|
|
102
|
+
db.pragma('journal_mode = WAL');
|
|
103
|
+
db.pragma('foreign_keys = ON');
|
|
104
|
+
|
|
105
|
+
return createTables(db);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Open or create a database file, ensuring the directory exists
|
|
110
|
+
* Creates tables if the database is new or ensures they exist
|
|
111
|
+
*/
|
|
112
|
+
export function openDb(dbPath) {
|
|
113
|
+
// Ensure the directory exists
|
|
114
|
+
const dbDir = path.dirname(dbPath);
|
|
115
|
+
if (!fs.existsSync(dbDir)) {
|
|
116
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if database file exists
|
|
120
|
+
const _dbExists = fs.existsSync(dbPath);
|
|
121
|
+
|
|
122
|
+
// Open or create the database
|
|
123
|
+
const db = new Database(dbPath);
|
|
124
|
+
db.pragma('journal_mode = WAL');
|
|
125
|
+
db.pragma('foreign_keys = ON');
|
|
126
|
+
|
|
127
|
+
// Create tables if database is new or tables don't exist
|
|
128
|
+
// Even if database exists, ensure tables exist (in case schema changed)
|
|
129
|
+
createTables(db);
|
|
130
|
+
|
|
131
|
+
return db;
|
|
132
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for logging HTTP packets to the database for forensic analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function getTimestampNs() {
|
|
6
|
+
return Number(process.hrtime.bigint());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getTimestampISO() {
|
|
10
|
+
return new Date().toISOString();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function calculateDurationMs(startNs, endNs) {
|
|
14
|
+
return (endNs - startNs) / 1_000_000;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize session ID from various header formats
|
|
19
|
+
* Supports: mcp-session-id, Mcp-Session-Id, X-MCP-Session-Id
|
|
20
|
+
*/
|
|
21
|
+
function normalizeSessionId(headers) {
|
|
22
|
+
if (!headers || typeof headers !== 'object') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check all possible header formats (case-insensitive)
|
|
27
|
+
const sessionHeaderKeys = [
|
|
28
|
+
'mcp-session-id',
|
|
29
|
+
'Mcp-Session-Id',
|
|
30
|
+
'X-MCP-Session-Id',
|
|
31
|
+
'x-mcp-session-id',
|
|
32
|
+
'MCP-Session-Id',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
for (const key of sessionHeaderKeys) {
|
|
36
|
+
if (headers[key]) {
|
|
37
|
+
return headers[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract JSON-RPC metadata from body
|
|
46
|
+
*/
|
|
47
|
+
function extractJsonRpcMetadata(bodyJson) {
|
|
48
|
+
if (!bodyJson) {
|
|
49
|
+
return { id: null, method: null, result: null, error: null };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const parsed = typeof bodyJson === 'string' ? JSON.parse(bodyJson) : bodyJson;
|
|
54
|
+
return {
|
|
55
|
+
id: parsed.id !== undefined ? String(parsed.id) : null,
|
|
56
|
+
method: parsed.method || null,
|
|
57
|
+
result: parsed.result ? JSON.stringify(parsed.result) : null,
|
|
58
|
+
error: parsed.error ? JSON.stringify(parsed.error) : null,
|
|
59
|
+
};
|
|
60
|
+
} catch {
|
|
61
|
+
return { id: null, method: null, result: null, error: null };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate info summary for quick packet identification
|
|
67
|
+
*/
|
|
68
|
+
function generateInfo(direction, method, url, statusCode, jsonrpcMethod) {
|
|
69
|
+
if (direction === 'request') {
|
|
70
|
+
const rpcInfo = jsonrpcMethod ? ` ${jsonrpcMethod}` : '';
|
|
71
|
+
return `${method} ${url}${rpcInfo}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rpcInfo = jsonrpcMethod ? ` ${jsonrpcMethod}` : '';
|
|
75
|
+
return `${statusCode}${rpcInfo}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Log an HTTP request packet
|
|
80
|
+
*/
|
|
81
|
+
function logRequestPacket(db, options) {
|
|
82
|
+
const { method, url, headers = {}, body, userAgent = null, remoteAddress = null } = options;
|
|
83
|
+
|
|
84
|
+
const timestampNs = getTimestampNs();
|
|
85
|
+
const timestampISO = getTimestampISO();
|
|
86
|
+
const sessionId = normalizeSessionId(headers);
|
|
87
|
+
const host = headers.host || headers.Host || null;
|
|
88
|
+
|
|
89
|
+
// Prepare body data
|
|
90
|
+
const { bodyRaw, bodyJson } = (() => {
|
|
91
|
+
if (!body) {
|
|
92
|
+
return { bodyRaw: '', bodyJson: null };
|
|
93
|
+
}
|
|
94
|
+
if (typeof body === 'string') {
|
|
95
|
+
return { bodyRaw: body, bodyJson: body };
|
|
96
|
+
}
|
|
97
|
+
if (typeof body === 'object') {
|
|
98
|
+
const raw = JSON.stringify(body);
|
|
99
|
+
return { bodyRaw: raw, bodyJson: raw };
|
|
100
|
+
}
|
|
101
|
+
return { bodyRaw: '', bodyJson: null };
|
|
102
|
+
})();
|
|
103
|
+
const headersJson = JSON.stringify(headers);
|
|
104
|
+
|
|
105
|
+
// Extract JSON-RPC metadata
|
|
106
|
+
const jsonrpc = extractJsonRpcMetadata(bodyJson || bodyRaw);
|
|
107
|
+
const jsonrpcId = jsonrpc.id;
|
|
108
|
+
const jsonrpcMethod = jsonrpc.method;
|
|
109
|
+
|
|
110
|
+
// Calculate packet length
|
|
111
|
+
const length = Buffer.byteLength(headersJson, 'utf8') + Buffer.byteLength(bodyRaw, 'utf8');
|
|
112
|
+
|
|
113
|
+
// Generate info summary
|
|
114
|
+
const info = generateInfo('request', method, url, null, jsonrpcMethod);
|
|
115
|
+
|
|
116
|
+
const stmt = db.prepare(`
|
|
117
|
+
INSERT INTO packets (
|
|
118
|
+
timestamp_ns,
|
|
119
|
+
timestamp_iso,
|
|
120
|
+
direction,
|
|
121
|
+
protocol,
|
|
122
|
+
session_id,
|
|
123
|
+
method,
|
|
124
|
+
url,
|
|
125
|
+
headers_json,
|
|
126
|
+
body_raw,
|
|
127
|
+
body_json,
|
|
128
|
+
jsonrpc_id,
|
|
129
|
+
jsonrpc_method,
|
|
130
|
+
length,
|
|
131
|
+
info,
|
|
132
|
+
user_agent,
|
|
133
|
+
remote_address,
|
|
134
|
+
host
|
|
135
|
+
) VALUES (?, ?, 'request', 'HTTP', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
136
|
+
`);
|
|
137
|
+
|
|
138
|
+
const result = stmt.run(
|
|
139
|
+
timestampNs,
|
|
140
|
+
timestampISO,
|
|
141
|
+
sessionId,
|
|
142
|
+
method,
|
|
143
|
+
url,
|
|
144
|
+
headersJson,
|
|
145
|
+
bodyRaw,
|
|
146
|
+
bodyJson,
|
|
147
|
+
jsonrpcId,
|
|
148
|
+
jsonrpcMethod,
|
|
149
|
+
length,
|
|
150
|
+
info,
|
|
151
|
+
userAgent,
|
|
152
|
+
remoteAddress,
|
|
153
|
+
host
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const frameNumber = result.lastInsertRowid;
|
|
157
|
+
|
|
158
|
+
// Update or create session record
|
|
159
|
+
if (sessionId) {
|
|
160
|
+
const sessionStmt = db.prepare(`
|
|
161
|
+
INSERT INTO sessions (session_id, first_seen_ns, last_seen_ns, packet_count, user_agent, remote_address, host)
|
|
162
|
+
VALUES (?, ?, ?, 1, ?, ?, ?)
|
|
163
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
164
|
+
last_seen_ns = excluded.last_seen_ns,
|
|
165
|
+
packet_count = packet_count + 1,
|
|
166
|
+
user_agent = COALESCE(excluded.user_agent, user_agent),
|
|
167
|
+
remote_address = COALESCE(excluded.remote_address, remote_address),
|
|
168
|
+
host = COALESCE(excluded.host, host)
|
|
169
|
+
`);
|
|
170
|
+
sessionStmt.run(sessionId, timestampNs, timestampNs, userAgent, remoteAddress, host);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create conversation entry for request
|
|
174
|
+
if (jsonrpcId) {
|
|
175
|
+
const convStmt = db.prepare(`
|
|
176
|
+
INSERT INTO conversations (
|
|
177
|
+
request_frame_number,
|
|
178
|
+
session_id,
|
|
179
|
+
jsonrpc_id,
|
|
180
|
+
method,
|
|
181
|
+
request_timestamp_ns,
|
|
182
|
+
status
|
|
183
|
+
) VALUES (?, ?, ?, ?, ?, 'pending')
|
|
184
|
+
`);
|
|
185
|
+
convStmt.run(frameNumber, sessionId, jsonrpcId, jsonrpcMethod || method, timestampNs);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { frameNumber, timestampNs };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Log an HTTP response packet
|
|
193
|
+
*/
|
|
194
|
+
function logResponsePacket(db, options) {
|
|
195
|
+
const {
|
|
196
|
+
statusCode,
|
|
197
|
+
headers = {},
|
|
198
|
+
body,
|
|
199
|
+
requestFrameNumber = null,
|
|
200
|
+
requestTimestampNs = null,
|
|
201
|
+
jsonrpcId = null,
|
|
202
|
+
userAgent = null,
|
|
203
|
+
remoteAddress = null,
|
|
204
|
+
} = options;
|
|
205
|
+
|
|
206
|
+
const timestampNs = getTimestampNs();
|
|
207
|
+
const timestampISO = getTimestampISO();
|
|
208
|
+
const sessionId = normalizeSessionId(headers);
|
|
209
|
+
const host = headers.host || headers.Host || null;
|
|
210
|
+
|
|
211
|
+
// Prepare body data
|
|
212
|
+
const { bodyRaw, bodyJson } = (() => {
|
|
213
|
+
if (!body) {
|
|
214
|
+
return { bodyRaw: '', bodyJson: null };
|
|
215
|
+
}
|
|
216
|
+
if (typeof body === 'string') {
|
|
217
|
+
return { bodyRaw: body, bodyJson: body };
|
|
218
|
+
}
|
|
219
|
+
if (typeof body === 'object') {
|
|
220
|
+
const raw = JSON.stringify(body);
|
|
221
|
+
return { bodyRaw: raw, bodyJson: raw };
|
|
222
|
+
}
|
|
223
|
+
return { bodyRaw: '', bodyJson: null };
|
|
224
|
+
})();
|
|
225
|
+
const headersJson = JSON.stringify(headers);
|
|
226
|
+
|
|
227
|
+
// Extract JSON-RPC metadata
|
|
228
|
+
const jsonrpc = extractJsonRpcMetadata(bodyJson || bodyRaw);
|
|
229
|
+
const jsonrpcIdFromBody = jsonrpc.id || jsonrpcId;
|
|
230
|
+
const jsonrpcMethod = jsonrpc.method;
|
|
231
|
+
const jsonrpcResult = jsonrpc.result;
|
|
232
|
+
const jsonrpcError = jsonrpc.error;
|
|
233
|
+
|
|
234
|
+
// Calculate packet length
|
|
235
|
+
const length = Buffer.byteLength(headersJson, 'utf8') + Buffer.byteLength(bodyRaw, 'utf8');
|
|
236
|
+
|
|
237
|
+
// Generate info summary
|
|
238
|
+
const info = generateInfo('response', null, null, statusCode, jsonrpcMethod);
|
|
239
|
+
|
|
240
|
+
const stmt = db.prepare(`
|
|
241
|
+
INSERT INTO packets (
|
|
242
|
+
timestamp_ns,
|
|
243
|
+
timestamp_iso,
|
|
244
|
+
direction,
|
|
245
|
+
protocol,
|
|
246
|
+
session_id,
|
|
247
|
+
status_code,
|
|
248
|
+
headers_json,
|
|
249
|
+
body_raw,
|
|
250
|
+
body_json,
|
|
251
|
+
jsonrpc_id,
|
|
252
|
+
jsonrpc_method,
|
|
253
|
+
jsonrpc_result,
|
|
254
|
+
jsonrpc_error,
|
|
255
|
+
length,
|
|
256
|
+
info,
|
|
257
|
+
user_agent,
|
|
258
|
+
remote_address,
|
|
259
|
+
host
|
|
260
|
+
) VALUES (?, ?, 'response', 'HTTP', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
261
|
+
`);
|
|
262
|
+
|
|
263
|
+
const result = stmt.run(
|
|
264
|
+
timestampNs,
|
|
265
|
+
timestampISO,
|
|
266
|
+
sessionId,
|
|
267
|
+
statusCode,
|
|
268
|
+
headersJson,
|
|
269
|
+
bodyRaw,
|
|
270
|
+
bodyJson,
|
|
271
|
+
jsonrpcIdFromBody,
|
|
272
|
+
jsonrpcMethod,
|
|
273
|
+
jsonrpcResult,
|
|
274
|
+
jsonrpcError,
|
|
275
|
+
length,
|
|
276
|
+
info,
|
|
277
|
+
userAgent,
|
|
278
|
+
remoteAddress,
|
|
279
|
+
host
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const frameNumber = result.lastInsertRowid;
|
|
283
|
+
|
|
284
|
+
// Update session record
|
|
285
|
+
if (sessionId) {
|
|
286
|
+
const sessionStmt = db.prepare(`
|
|
287
|
+
INSERT INTO sessions (session_id, first_seen_ns, last_seen_ns, packet_count, user_agent, remote_address, host)
|
|
288
|
+
VALUES (?, ?, ?, 1, ?, ?, ?)
|
|
289
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
290
|
+
last_seen_ns = excluded.last_seen_ns,
|
|
291
|
+
packet_count = packet_count + 1,
|
|
292
|
+
user_agent = COALESCE(excluded.user_agent, user_agent),
|
|
293
|
+
remote_address = COALESCE(excluded.remote_address, remote_address),
|
|
294
|
+
host = COALESCE(excluded.host, host)
|
|
295
|
+
`);
|
|
296
|
+
sessionStmt.run(sessionId, timestampNs, timestampNs, userAgent, remoteAddress, host);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Update conversation entry with response
|
|
300
|
+
if (jsonrpcIdFromBody || requestFrameNumber) {
|
|
301
|
+
const durationMs = requestTimestampNs
|
|
302
|
+
? calculateDurationMs(requestTimestampNs, timestampNs)
|
|
303
|
+
: null;
|
|
304
|
+
|
|
305
|
+
const status = statusCode >= 200 && statusCode < 300 ? 'completed' : 'error';
|
|
306
|
+
|
|
307
|
+
if (requestFrameNumber) {
|
|
308
|
+
// Update existing conversation
|
|
309
|
+
const updateConvStmt = db.prepare(`
|
|
310
|
+
UPDATE conversations
|
|
311
|
+
SET response_frame_number = ?,
|
|
312
|
+
response_timestamp_ns = ?,
|
|
313
|
+
duration_ms = ?,
|
|
314
|
+
status = ?
|
|
315
|
+
WHERE request_frame_number = ?
|
|
316
|
+
`);
|
|
317
|
+
updateConvStmt.run(frameNumber, timestampNs, durationMs, status, requestFrameNumber);
|
|
318
|
+
} else if (jsonrpcIdFromBody) {
|
|
319
|
+
// Try to find conversation by JSON-RPC ID
|
|
320
|
+
const findConvStmt = db.prepare(`
|
|
321
|
+
SELECT request_frame_number FROM conversations
|
|
322
|
+
WHERE jsonrpc_id = ? AND response_frame_number IS NULL
|
|
323
|
+
ORDER BY request_timestamp_ns DESC
|
|
324
|
+
LIMIT 1
|
|
325
|
+
`);
|
|
326
|
+
const conv = findConvStmt.get(jsonrpcIdFromBody);
|
|
327
|
+
if (conv) {
|
|
328
|
+
const updateConvStmt = db.prepare(`
|
|
329
|
+
UPDATE conversations
|
|
330
|
+
SET response_frame_number = ?,
|
|
331
|
+
response_timestamp_ns = ?,
|
|
332
|
+
duration_ms = ?,
|
|
333
|
+
status = ?
|
|
334
|
+
WHERE request_frame_number = ?
|
|
335
|
+
`);
|
|
336
|
+
updateConvStmt.run(frameNumber, timestampNs, durationMs, status, conv.request_frame_number);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return frameNumber;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function getLogger(db) {
|
|
345
|
+
return {
|
|
346
|
+
logRequestPacket: logRequestPacket.bind(null, db),
|
|
347
|
+
logResponsePacket: logResponsePacket.bind(null, db),
|
|
348
|
+
};
|
|
349
|
+
}
|