# Directory Structure
```
├── .gitignore
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── sharkmcp-configs.json
├── src
│ ├── index.ts
│ ├── tools
│ │ ├── analyze-pcap-file.ts
│ │ ├── manage-config.ts
│ │ ├── start-capture-session.ts
│ │ └── stop-capture-session.ts
│ ├── types.ts
│ └── utils.ts
├── test
│ ├── dump.pcapng
│ └── integration.test.js
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 | npm-debug.log*
4 | pnpm-debug.log*
5 |
6 | # Build outputs
7 | dist/
8 | build/
9 | *.tsbuildinfo
10 |
11 | *.log
12 | *.pid
13 | *.seed
14 | *.lock
15 | .DS_Store
16 | Thumbs.db
17 |
18 | # OS generated files
19 | .DS_Store
20 | .DS_Store?
21 | ._*
22 | .Spotlight-V100
23 | .Trashes
24 | ehthumbs.db
25 |
26 | /captures/
27 | sslkeylog.log
28 |
29 | # sharkmcp-configs.json
```
--------------------------------------------------------------------------------
/sharkmcp-configs.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": "1.0.0",
3 | "configs": {
4 | "tls_analysis": {
5 | "name": "tls_analysis",
6 | "description": "TLS traffic analysis with handshake details",
7 | "captureFilter": "port 443",
8 | "displayFilter": "tls.handshake",
9 | "outputFormat": "json",
10 | "maxPackets": 500,
11 | "interface": "en0"
12 | },
13 | "integration_test": {
14 | "name": "integration_test",
15 | "description": "Short capture for integration testing",
16 | "captureFilter": "",
17 | "displayFilter": "",
18 | "outputFormat": "json",
19 | "maxPackets": 100,
20 | "timeout": 10,
21 | "interface": "en0"
22 | }
23 | }
24 | }
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "outDir": "./dist",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "allowSyntheticDefaultImports": true,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "declaration": true,
14 | "declarationMap": true,
15 | "sourceMap": true,
16 | "removeComments": false,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "exactOptionalPropertyTypes": true,
20 | "noImplicitReturns": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUncheckedIndexedAccess": true,
23 | "noImplicitOverride": true,
24 | "allowUnusedLabels": false,
25 | "allowUnreachableCode": false,
26 | "resolveJsonModule": true,
27 | "isolatedModules": true,
28 | "verbatimModuleSyntax": false
29 | },
30 | "include": [
31 | "src/**/*"
32 | ],
33 | "exclude": [
34 | "node_modules",
35 | "dist",
36 | "test"
37 | ]
38 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "SharkMCP",
3 | "version": "0.1.0",
4 | "description": "A Wireshark MCP server for network packet analysis",
5 | "main": "src/index.ts",
6 | "type": "module",
7 | "scripts": {
8 | "build": "tsc",
9 | "dev": "node --loader ts-node/esm src/index.ts",
10 | "start": "node dist/index.js",
11 | "test": "pnpm run test:integration",
12 | "test:unit": "echo 'Unit tests not yet implemented'",
13 | "test:integration": "pnpm run build && node test/integration.test.js",
14 | "test:direct": "pnpm run build && node test-client.js"
15 | },
16 | "keywords": [
17 | "sharkmcp",
18 | "wireshark",
19 | "mcp",
20 | "network",
21 | "security",
22 | "packet-analysis",
23 | "tshark"
24 | ],
25 | "author": "",
26 | "license": "ISC",
27 | "packageManager": "[email protected]",
28 | "dependencies": {
29 | "@modelcontextprotocol/sdk": "^1.12.1",
30 | "@types/node": "24.0.0",
31 | "axios": "1.9.0",
32 | "which": "5.0.0",
33 | "zod": "^3.25.61"
34 | },
35 | "devDependencies": {
36 | "@types/which": "^3.0.4",
37 | "ts-node": "^10.9.2",
38 | "tsx": "^4.20.1",
39 | "typescript": "^5.8.3"
40 | }
41 | }
42 |
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ChildProcess } from "child_process";
2 |
3 | /**
4 | * Represents an active packet capture session
5 | */
6 | export interface CaptureSession {
7 | id: string;
8 | process: ChildProcess | null;
9 | interface: string;
10 | captureFilter?: string;
11 | timeout: number;
12 | maxPackets: number;
13 | startTime: Date;
14 | tempFile: string;
15 | status: 'running' | 'completed' | 'error';
16 | endTime?: Date;
17 | exitCode?: number;
18 | }
19 |
20 | /**
21 | * Output format options for packet analysis
22 | */
23 | export type OutputFormat = 'json' | 'fields' | 'text';
24 |
25 | /**
26 | * Environment configuration for tshark processes
27 | */
28 | export interface TsharkEnvironment {
29 | [key: string]: string;
30 | }
31 |
32 | /**
33 | * Configuration for PCAP analysis
34 | */
35 | export interface AnalysisConfig {
36 | filePath: string;
37 | displayFilter: string;
38 | outputFormat: OutputFormat;
39 | customFields?: string;
40 | sslKeylogFile?: string;
41 | }
42 |
43 | /**
44 | * Reusable filter configuration that LLMs can save and reuse
45 | */
46 | export interface FilterConfig {
47 | name: string;
48 | description?: string;
49 | captureFilter?: string;
50 | displayFilter?: string;
51 | outputFormat?: OutputFormat;
52 | customFields?: string;
53 | timeout?: number;
54 | maxPackets?: number;
55 | interface?: string;
56 | }
57 |
58 | /**
59 | * Config file structure
60 | */
61 | export interface ConfigFile {
62 | version: string;
63 | configs: { [name: string]: FilterConfig };
64 | }
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import { CaptureSession } from "./types.js";
4 | import { startCaptureSessionSchema, startCaptureSessionHandler } from "./tools/start-capture-session.js";
5 | import { stopCaptureSessionSchema, stopCaptureSessionHandler } from "./tools/stop-capture-session.js";
6 | import { analyzePcapFileSchema, analyzePcapFileHandler } from "./tools/analyze-pcap-file.js";
7 | import { manageConfigSchema, manageConfigHandler } from "./tools/manage-config.js";
8 |
9 | // Active capture sessions storage
10 | const activeSessions = new Map<string, CaptureSession>();
11 |
12 | // Initialize MCP server
13 | const server = new McpServer({
14 | name: 'SharkMCP',
15 | version: '0.1.0',
16 | });
17 |
18 | /**
19 | * Register all tools with the MCP server
20 | * Each tool is defined in its own module for better organization
21 | */
22 |
23 | // Tool 1: Start background packet capture session
24 | server.tool(
25 | 'start_capture_session',
26 | 'Start a background packet capture session. LLMs control all capture parameters including filters, interfaces, and packet limits. Can use saved configurations.',
27 | startCaptureSessionSchema,
28 | async (args) => startCaptureSessionHandler(args, activeSessions)
29 | );
30 |
31 | // Tool 2: Stop capture session and retrieve results
32 | server.tool(
33 | 'stop_capture_session',
34 | 'Stop a running capture session and analyze packets. LLMs control all analysis parameters including display filters and output formats. Can use saved configurations.',
35 | stopCaptureSessionSchema,
36 | async (args) => stopCaptureSessionHandler(args, activeSessions)
37 | );
38 |
39 | // Tool 3: Analyze an existing PCAP file
40 | server.tool(
41 | 'analyze_pcap_file',
42 | 'Analyze a local pcap/pcapng file. LLMs control all analysis parameters including filters, output formats, and custom fields. Can use saved configurations.',
43 | analyzePcapFileSchema,
44 | async (args) => analyzePcapFileHandler(args)
45 | );
46 |
47 | // Tool 4: Manage filter configurations
48 | server.tool(
49 | 'manage_config',
50 | 'Save, load, list, or delete reusable filter configurations. Allows LLMs to store commonly used capture and analysis parameters for easy reuse.',
51 | manageConfigSchema,
52 | async (args) => manageConfigHandler(args)
53 | );
54 |
55 | // Start receiving messages on stdin and sending messages on stdout
56 | const transport = new StdioServerTransport();
57 | await server.connect(transport);
58 |
59 | console.error("SharkMCP server is running and connected to transport. Ready for requests.");
```
--------------------------------------------------------------------------------
/src/tools/analyze-pcap-file.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import fs from "fs/promises";
3 | import { analyzePcap, trimOutput, loadFilterConfig } from "../utils.js";
4 |
5 | /**
6 | * Input schema for analyze pcap file tool
7 | */
8 | export const analyzePcapFileSchema = {
9 | filePath: z.string().describe('Path to the local .pcap or .pcapng file to analyze.'),
10 | displayFilter: z.string().optional().describe('Wireshark display filter for analysis (e.g., "tls.handshake.type == 1")'),
11 | outputFormat: z.enum(['json', 'fields', 'text']).optional().default('text').describe('Output format: json (-T json), fields (custom -e), or text (default wireshark output)'),
12 | customFields: z.string().optional().describe('Custom tshark field list (only used with outputFormat=fields)'),
13 | sslKeylogFile: z.string().optional().describe('ABSOLUTE path to SSL keylog file for TLS decryption'),
14 | configName: z.string().optional().describe('Name of saved configuration to use for analysis parameters')
15 | };
16 |
17 | /**
18 | * Tool handler for analyzing an existing PCAP file
19 | * This tool analyzes pre-existing PCAP/PCAPNG files without needing to capture
20 | */
21 | export async function analyzePcapFileHandler(args: any) {
22 | try {
23 | let { filePath, displayFilter, outputFormat, customFields, sslKeylogFile, configName } = args;
24 |
25 | // If configName is provided, load and use that configuration for analysis
26 | if (configName) {
27 | const savedConfig = await loadFilterConfig(configName);
28 | if (!savedConfig) {
29 | return {
30 | content: [{
31 | type: 'text' as const,
32 | text: `Error: Configuration '${configName}' not found. Use manage_config with action 'list' to see available configurations.`,
33 | }],
34 | isError: true
35 | };
36 | }
37 |
38 | // Override analysis parameters with saved config (saved config takes precedence)
39 | if (savedConfig.displayFilter) displayFilter = savedConfig.displayFilter;
40 | if (savedConfig.outputFormat) outputFormat = savedConfig.outputFormat;
41 | if (savedConfig.customFields) customFields = savedConfig.customFields;
42 |
43 | console.error(`Using saved configuration '${configName}' for analysis: ${JSON.stringify(savedConfig)}`);
44 | }
45 |
46 | // Verify file exists before proceeding
47 | await fs.access(filePath);
48 |
49 | // Analyze the file using the reusable function
50 | const output = await analyzePcap(
51 | filePath,
52 | displayFilter,
53 | outputFormat,
54 | customFields,
55 | sslKeylogFile
56 | );
57 |
58 | const keylogToUse = sslKeylogFile || process.env.SSLKEYLOGFILE;
59 |
60 | // Trim output if too large
61 | const trimmedOutput = trimOutput(output, outputFormat);
62 |
63 | const configInfo = configName ? `\nUsing saved config: ${configName}` : '';
64 |
65 | return {
66 | content: [{
67 | type: 'text' as const,
68 | text: `Analysis of '${filePath}' complete!${configInfo}\nDisplay Filter: ${displayFilter || 'none'}\nOutput Format: ${outputFormat}\nSSL Decryption: ${keylogToUse ? 'Enabled' : 'Disabled'}\n\nPacket Analysis Results:\n${trimmedOutput}`,
69 | }],
70 | };
71 | } catch (error: any) {
72 | console.error(`Error analyzing PCAP file: ${error.message}`);
73 | return {
74 | content: [{ type: 'text' as const, text: `Error: ${error.message}` }],
75 | isError: true
76 | };
77 | }
78 | }
```
--------------------------------------------------------------------------------
/src/tools/stop-capture-session.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import fs from "fs/promises";
3 | import { CaptureSession } from "../types.js";
4 | import { analyzePcap, trimOutput, loadFilterConfig } from "../utils.js";
5 |
6 | /**
7 | * Input schema for stop capture session tool
8 | */
9 | export const stopCaptureSessionSchema = {
10 | sessionId: z.string().describe('Session ID returned from start_capture_session'),
11 | displayFilter: z.string().optional().describe('Wireshark display filter for analysis (e.g., "tls.handshake.type == 1")'),
12 | outputFormat: z.enum(['json', 'fields', 'text']).optional().default('text').describe('Output format: json (-T json), fields (custom -e), or text (default wireshark output)'),
13 | customFields: z.string().optional().describe('Custom tshark field list (only used with outputFormat=fields)'),
14 | sslKeylogFile: z.string().optional().describe('ABSOLUTE path to SSL keylog file for TLS decryption'),
15 | configName: z.string().optional().describe('Name of saved configuration to use for analysis parameters')
16 | };
17 |
18 | /**
19 | * Tool handler for stopping capture session and retrieving results
20 | * This tool stops a running capture session and analyzes the captured packets
21 | */
22 | export async function stopCaptureSessionHandler(args: any, activeSessions: Map<string, CaptureSession>) {
23 | try {
24 | let { sessionId, displayFilter, outputFormat, customFields, sslKeylogFile, configName } = args;
25 | const session = activeSessions.get(sessionId);
26 |
27 | if (!session) {
28 | return {
29 | content: [{
30 | type: 'text' as const,
31 | text: `Error: No active session found with ID '${sessionId}'. Use 'list_capture_sessions' to see active sessions.`,
32 | }],
33 | isError: true
34 | };
35 | }
36 |
37 | // If configName is provided, load and use that configuration for analysis
38 | if (configName) {
39 | const savedConfig = await loadFilterConfig(configName);
40 | if (!savedConfig) {
41 | return {
42 | content: [{
43 | type: 'text' as const,
44 | text: `Error: Configuration '${configName}' not found. Use manage_config with action 'list' to see available configurations.`,
45 | }],
46 | isError: true
47 | };
48 | }
49 |
50 | // Override analysis parameters with saved config (saved config takes precedence)
51 | if (savedConfig.displayFilter) displayFilter = savedConfig.displayFilter;
52 | if (savedConfig.outputFormat) outputFormat = savedConfig.outputFormat;
53 | if (savedConfig.customFields) customFields = savedConfig.customFields;
54 |
55 | console.error(`Using saved configuration '${configName}' for analysis: ${JSON.stringify(savedConfig)}`);
56 | }
57 |
58 | console.error(`Stopping capture session: ${sessionId}`);
59 |
60 | // Check if the capture process has already completed naturally
61 | if (session.process && !session.process.killed && session.status === 'running') {
62 | console.error(`Terminating capture process for session ${sessionId}`);
63 | session.process.kill('SIGTERM');
64 | // Wait a moment for graceful termination
65 | await new Promise(resolve => setTimeout(resolve, 2000));
66 | } else if (session.status === 'completed') {
67 | console.error(`Capture session ${sessionId} already completed naturally`);
68 | } else {
69 | console.error(`Capture session ${sessionId} process already terminated`);
70 | }
71 |
72 | // Remove from active sessions
73 | activeSessions.delete(sessionId);
74 |
75 | try {
76 | // Check if file exists
77 | await fs.access(session.tempFile);
78 |
79 | // Wait a bit more to ensure file is fully written
80 | await new Promise(resolve => setTimeout(resolve, 1000));
81 |
82 | // Analyze captured file using the reusable function
83 | const output = await analyzePcap(
84 | session.tempFile,
85 | displayFilter,
86 | outputFormat,
87 | customFields,
88 | sslKeylogFile
89 | );
90 |
91 | const keylogToUse = sslKeylogFile || process.env.SSLKEYLOGFILE;
92 |
93 | // Clean up temporary file
94 | await fs.unlink(session.tempFile).catch(err =>
95 | console.error(`Failed to delete ${session.tempFile}: ${err.message}`)
96 | );
97 |
98 | const duration = new Date().getTime() - session.startTime.getTime();
99 | const durationSec = (duration / 1000).toFixed(1);
100 |
101 | // Trim output if too large
102 | const trimmedOutput = trimOutput(output, outputFormat);
103 |
104 | const configInfo = configName ? `\nUsing saved config: ${configName}` : '';
105 |
106 | return {
107 | content: [{
108 | type: 'text' as const,
109 | text: `Capture session '${sessionId}' completed!${configInfo}\nInterface: ${session.interface}\nDuration: ${durationSec}s\nDisplay Filter: ${displayFilter || 'none'}\nOutput Format: ${outputFormat}\nSSL Decryption: ${keylogToUse ? 'Enabled' : 'Disabled'}\n\nPacket Analysis Results:\n${trimmedOutput}`,
110 | }],
111 | };
112 |
113 | } catch (fileError: any) {
114 | console.error(`Error analyzing session ${sessionId}: ${fileError.message}`);
115 | return {
116 | content: [{
117 | type: 'text' as const,
118 | text: `Error analyzing session '${sessionId}': Capture file not found or unreadable. This could mean no packets were captured.\nDetails: ${fileError.message}`,
119 | }],
120 | isError: true,
121 | };
122 | }
123 | } catch (error: any) {
124 | console.error(`Error stopping capture session: ${error.message}`);
125 | return {
126 | content: [{ type: 'text' as const, text: `Error: ${error.message}` }],
127 | isError: true
128 | };
129 | }
130 | }
```
--------------------------------------------------------------------------------
/src/tools/start-capture-session.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { spawn } from "child_process";
3 | import { CaptureSession } from "../types.js";
4 | import { findTshark, loadFilterConfig } from "../utils.js";
5 |
6 | /**
7 | * Input schema for start capture session tool
8 | */
9 | export const startCaptureSessionSchema = {
10 | interface: z.string().optional().default('lo0').describe('Network interface to capture from (e.g., eth0, en0, lo0)'),
11 | captureFilter: z.string().optional().describe('Optional BPF capture filter to apply while capturing (e.g., "port 443")'),
12 | timeout: z.number().optional().default(60).describe('Timeout in seconds before auto-stopping capture (default: 60s to prevent orphaned sessions)'),
13 | maxPackets: z.number().optional().default(100000).describe('Maximum number of packets to capture (safety limit, default: 100,000)'),
14 | sessionName: z.string().optional().describe('Optional session name for easier identification'),
15 | configName: z.string().optional().describe('Name of saved configuration to use (will override other parameters)')
16 | };
17 |
18 | /**
19 | * Tool handler for starting background packet capture session
20 | * This tool starts a detached tshark process to capture network packets
21 | */
22 | export async function startCaptureSessionHandler(args: any, activeSessions: Map<string, CaptureSession>) {
23 | try {
24 | const tsharkPath = await findTshark();
25 | let { interface: networkInterface, captureFilter, timeout, maxPackets, sessionName, configName } = args;
26 |
27 | // If configName is provided, load and use that configuration
28 | if (configName) {
29 | const savedConfig = await loadFilterConfig(configName);
30 | if (!savedConfig) {
31 | return {
32 | content: [{
33 | type: 'text' as const,
34 | text: `Error: Configuration '${configName}' not found. Use manage_config with action 'list' to see available configurations.`,
35 | }],
36 | isError: true
37 | };
38 | }
39 |
40 | // Override parameters with saved config (saved config takes precedence)
41 | if (savedConfig.interface) networkInterface = savedConfig.interface;
42 | if (savedConfig.captureFilter) captureFilter = savedConfig.captureFilter;
43 | if (savedConfig.timeout) timeout = savedConfig.timeout;
44 | if (savedConfig.maxPackets) maxPackets = savedConfig.maxPackets;
45 |
46 | console.error(`Using saved configuration '${configName}': ${JSON.stringify(savedConfig)}`);
47 | }
48 |
49 | // Generate unique session ID
50 | const sessionId = sessionName || `capture_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
51 |
52 | // Check if session already exists
53 | if (activeSessions.has(sessionId)) {
54 | return {
55 | content: [{
56 | type: 'text' as const,
57 | text: `Error: Session '${sessionId}' already exists. Use a different session name or stop the existing session.`,
58 | }],
59 | isError: true
60 | };
61 | }
62 |
63 | const tempFile = `/tmp/shark_${sessionId}.pcap`;
64 | console.error(`Starting capture session: ${sessionId} on ${networkInterface}`);
65 |
66 | // Build comprehensive tshark command for background capture
67 | // Use timeout as primary stopping mechanism, with maxPackets as safety limit
68 | const args_array = [
69 | '-i', networkInterface,
70 | '-a', `duration:${timeout}`, // Auto-stop after timeout seconds
71 | '-c', maxPackets.toString(), // Safety limit to prevent excessive capture
72 | '-w', tempFile
73 | ];
74 |
75 | // Add capture filter if provided (as a single argument to -f)
76 | if (captureFilter) {
77 | args_array.push('-f', captureFilter);
78 | }
79 |
80 | // Set up basic environment
81 | const captureEnv: Record<string, string> = {
82 | ...process.env,
83 | PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin`
84 | };
85 |
86 | // Log the command with proper quoting for copy-paste debugging
87 | const quotedArgs = args_array.map(arg => {
88 | // Quote arguments that contain spaces or special characters
89 | if (arg.includes(' ') || arg.includes('|') || arg.includes('&') || arg.includes('(') || arg.includes(')')) {
90 | return `"${arg}"`;
91 | }
92 | return arg;
93 | });
94 | console.error(`Running background command: ${tsharkPath} ${quotedArgs.join(' ')}`);
95 |
96 | // Start background capture process with stderr logging
97 | const captureProcess = spawn(tsharkPath, args_array, {
98 | env: captureEnv,
99 | stdio: ['ignore', 'ignore', 'pipe'], // Capture stderr for error logging
100 | detached: true // Fully detach the process
101 | });
102 |
103 | // Log any errors from tshark
104 | if (captureProcess.stderr) {
105 | captureProcess.stderr.on('data', (data) => {
106 | console.error(`tshark stderr [${sessionId}]: ${data.toString().trim()}`);
107 | });
108 | }
109 |
110 | // Unref the process so the parent can exit independently
111 | captureProcess.unref();
112 |
113 | // Store session info
114 | const session: CaptureSession = {
115 | id: sessionId,
116 | process: captureProcess,
117 | interface: networkInterface,
118 | captureFilter,
119 | timeout,
120 | maxPackets,
121 | startTime: new Date(),
122 | tempFile,
123 | status: 'running'
124 | };
125 |
126 | activeSessions.set(sessionId, session);
127 |
128 | // Handle process completion - KEEP SESSION ALIVE for result retrieval
129 | captureProcess.on('exit', (code) => {
130 | console.error(`Capture session ${sessionId} exited with code: ${code}`);
131 | if (activeSessions.has(sessionId)) {
132 | const session = activeSessions.get(sessionId)!;
133 | session.process = null;
134 | session.status = code === 0 ? 'completed' : 'error';
135 | session.endTime = new Date();
136 | if (code !== null) {
137 | session.exitCode = code;
138 | }
139 | console.error(`Session ${sessionId} marked as ${session.status}, file: ${session.tempFile}`);
140 | }
141 | });
142 |
143 | captureProcess.on('error', (error) => {
144 | console.error(`Capture session ${sessionId} error: ${error.message}`);
145 | if (activeSessions.has(sessionId)) {
146 | const session = activeSessions.get(sessionId)!;
147 | session.process = null;
148 | session.status = 'error';
149 | session.endTime = new Date();
150 | }
151 | });
152 |
153 | const configInfo = configName ? `\nUsing saved config: ${configName}` : '';
154 |
155 | return {
156 | content: [{
157 | type: 'text' as const,
158 | text: `Capture session started successfully!${configInfo}\nSession ID: ${sessionId}\nInterface: ${networkInterface}\nCapture Filter: ${captureFilter || 'none'}\nTimeout: ${timeout}s (auto-stop)\nMax Packets: ${maxPackets} (safety limit)\n\nCapture will auto-stop after ${timeout} seconds or use 'stop_capture_session' with session ID '${sessionId}' to stop manually and retrieve results.`,
159 | }],
160 | };
161 | } catch (error: any) {
162 | console.error(`Error starting capture session: ${error.message}`);
163 | return {
164 | content: [{ type: 'text' as const, text: `Error: ${error.message}` }],
165 | isError: true
166 | };
167 | }
168 | }
```
--------------------------------------------------------------------------------
/src/tools/manage-config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { FilterConfig } from "../types.js";
3 | import {
4 | saveFilterConfig,
5 | loadFilterConfig,
6 | listFilterConfigs,
7 | deleteFilterConfig
8 | } from "../utils.js";
9 |
10 | /**
11 | * Input schema for config management tool
12 | */
13 | export const manageConfigSchema = {
14 | action: z.enum(['save', 'load', 'list', 'view', 'delete']).describe('Action to perform: save, load, list (brief), view (detailed), or delete a configuration'),
15 | name: z.string().optional().describe('Name of the configuration (required for save, load, delete)'),
16 | detailed: z.boolean().optional().default(false).describe('Show detailed configuration info when listing (only used with list action)'),
17 | config: z.object({
18 | description: z.string().optional().describe('Description of what this config does'),
19 | captureFilter: z.string().optional().describe('BPF capture filter for packet capture'),
20 | displayFilter: z.string().optional().describe('Wireshark display filter for analysis'),
21 | outputFormat: z.enum(['json', 'fields', 'text']).optional().describe('Output format for analysis'),
22 | customFields: z.string().optional().describe('Custom field list for fields format'),
23 | timeout: z.number().optional().describe('Timeout in seconds for capture sessions'),
24 | maxPackets: z.number().optional().describe('Maximum packets to capture'),
25 | interface: z.string().optional().describe('Network interface to use')
26 | }).optional().describe('Configuration object (required for save action)')
27 | };
28 |
29 | /**
30 | * Tool handler for managing filter configurations
31 | * Allows LLMs to save, load, list, and delete reusable filter configurations
32 | */
33 | export async function manageConfigHandler(args: any) {
34 | try {
35 | const { action, name, config, detailed } = args;
36 |
37 | switch (action) {
38 | case 'save':
39 | if (!name || !config) {
40 | return {
41 | content: [{
42 | type: 'text' as const,
43 | text: 'Error: Both name and config are required for save action.',
44 | }],
45 | isError: true
46 | };
47 | }
48 |
49 | const filterConfig: FilterConfig = {
50 | name,
51 | ...config
52 | };
53 |
54 | await saveFilterConfig(filterConfig);
55 | return {
56 | content: [{
57 | type: 'text' as const,
58 | text: `Configuration '${name}' saved successfully!\n\nSaved config:\n${JSON.stringify(filterConfig, null, 2)}`,
59 | }],
60 | };
61 |
62 | case 'load':
63 | if (!name) {
64 | return {
65 | content: [{
66 | type: 'text' as const,
67 | text: 'Error: Name is required for load action.',
68 | }],
69 | isError: true
70 | };
71 | }
72 |
73 | const loadedConfig = await loadFilterConfig(name);
74 | if (!loadedConfig) {
75 | return {
76 | content: [{
77 | type: 'text' as const,
78 | text: `Error: Configuration '${name}' not found.`,
79 | }],
80 | isError: true
81 | };
82 | }
83 |
84 | return {
85 | content: [{
86 | type: 'text' as const,
87 | text: `Configuration '${name}' loaded:\n\n${JSON.stringify(loadedConfig, null, 2)}`,
88 | }],
89 | };
90 |
91 | case 'list':
92 | const allConfigs = await listFilterConfigs();
93 | if (allConfigs.length === 0) {
94 | return {
95 | content: [{
96 | type: 'text' as const,
97 | text: 'No saved configurations found.',
98 | }],
99 | };
100 | }
101 |
102 | if (detailed) {
103 | // Show detailed information for all configurations
104 | const detailedList = allConfigs.map(cfg => {
105 | const configDetails = [
106 | `Name: ${cfg.name}`,
107 | ...(cfg.description ? [`Description: ${cfg.description}`] : []),
108 | ...(cfg.captureFilter ? [`Capture Filter: ${cfg.captureFilter}`] : []),
109 | ...(cfg.displayFilter ? [`Display Filter: ${cfg.displayFilter}`] : []),
110 | ...(cfg.outputFormat ? [`Output Format: ${cfg.outputFormat}`] : []),
111 | ...(cfg.customFields ? [`Custom Fields: ${cfg.customFields}`] : []),
112 | ...(cfg.interface ? [`Interface: ${cfg.interface}`] : []),
113 | ...(cfg.timeout ? [`Timeout: ${cfg.timeout}s`] : []),
114 | ...(cfg.maxPackets ? [`Max Packets: ${cfg.maxPackets}`] : [])
115 | ];
116 | return configDetails.join('\n ');
117 | }).join('\n\n' + '─'.repeat(50) + '\n\n');
118 |
119 | return {
120 | content: [{
121 | type: 'text' as const,
122 | text: `Available configurations (${allConfigs.length}) - Detailed View:\n\n${'─'.repeat(50)}\n\n${detailedList}\n\n${'─'.repeat(50)}\n\nUse 'load' action with a specific name to get the full JSON configuration.`,
123 | }],
124 | };
125 | } else {
126 | // Show brief list (existing behavior)
127 | const configList = allConfigs.map(cfg =>
128 | `• ${cfg.name}${cfg.description ? `: ${cfg.description}` : ''}`
129 | ).join('\n');
130 |
131 | return {
132 | content: [{
133 | type: 'text' as const,
134 | text: `Available configurations (${allConfigs.length}):\n\n${configList}\n\nUse 'load' action to get full details of any configuration, or use 'view' action to see all configurations with full details.`,
135 | }],
136 | };
137 | }
138 |
139 | case 'view':
140 | const allConfigsForView = await listFilterConfigs();
141 | if (allConfigsForView.length === 0) {
142 | return {
143 | content: [{
144 | type: 'text' as const,
145 | text: 'No saved configurations found.',
146 | }],
147 | };
148 | }
149 |
150 | const configDetails = allConfigsForView.map(cfg =>
151 | `${cfg.name}:\n${JSON.stringify(cfg, null, 2)}`
152 | ).join('\n\n' + '─'.repeat(60) + '\n\n');
153 |
154 | return {
155 | content: [{
156 | type: 'text' as const,
157 | text: `All configurations (${allConfigsForView.length}) - Full Details:\n\n${'─'.repeat(60)}\n\n${configDetails}\n\n${'─'.repeat(60)}`,
158 | }],
159 | };
160 |
161 | case 'delete':
162 | if (!name) {
163 | return {
164 | content: [{
165 | type: 'text' as const,
166 | text: 'Error: Name is required for delete action.',
167 | }],
168 | isError: true
169 | };
170 | }
171 |
172 | const deleted = await deleteFilterConfig(name);
173 | if (!deleted) {
174 | return {
175 | content: [{
176 | type: 'text' as const,
177 | text: `Error: Configuration '${name}' not found.`,
178 | }],
179 | isError: true
180 | };
181 | }
182 |
183 | return {
184 | content: [{
185 | type: 'text' as const,
186 | text: `Configuration '${name}' deleted successfully.`,
187 | }],
188 | };
189 |
190 | default:
191 | return {
192 | content: [{
193 | type: 'text' as const,
194 | text: `Error: Unknown action '${action}'. Use save, load, list, view, or delete.`,
195 | }],
196 | isError: true
197 | };
198 | }
199 | } catch (error: any) {
200 | console.error(`Error managing config: ${error.message}`);
201 | return {
202 | content: [{ type: 'text' as const, text: `Error: ${error.message}` }],
203 | isError: true
204 | };
205 | }
206 | }
```
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { promisify } from "util";
2 | import { exec } from "child_process";
3 | import which from "which";
4 | import fs from "fs/promises";
5 | import path from "path";
6 | import { fileURLToPath } from "url";
7 | import { OutputFormat, TsharkEnvironment, FilterConfig, ConfigFile } from "./types.js";
8 |
9 | // Promisify exec for async/await usage
10 | const execAsync = promisify(exec);
11 |
12 | // Get the directory of this file and construct config path relative to project root
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = path.dirname(__filename);
15 | const CONFIG_FILE_PATH = path.join(__dirname, '..', 'sharkmcp-configs.json');
16 |
17 | /**
18 | * Dynamically locate tshark executable with cross-platform support
19 | */
20 | export async function findTshark(): Promise<string> {
21 | // First, try to find tshark in PATH
22 | try {
23 | const tsharkPath = await which('tshark');
24 | if (!tsharkPath) {
25 | throw new Error('tshark not found in PATH');
26 | }
27 | const pathString = Array.isArray(tsharkPath) ? tsharkPath[0] : tsharkPath;
28 |
29 | // Verify the executable works
30 | await execAsync(`"${pathString}" -v`, { timeout: 5000 });
31 | console.error(`Found tshark at: ${pathString}`);
32 | return pathString;
33 | } catch (err: any) {
34 | console.error('tshark not found in PATH, trying fallback locations...');
35 | }
36 |
37 | // Platform-specific fallback paths
38 | const getFallbackPaths = (): string[] => {
39 | switch (process.platform) {
40 | case 'win32':
41 | return [
42 | 'C:\\Program Files\\Wireshark\\tshark.exe',
43 | 'C:\\Program Files (x86)\\Wireshark\\tshark.exe',
44 | ...(process.env.ProgramFiles ? [`${process.env.ProgramFiles}\\Wireshark\\tshark.exe`] : []),
45 | ...(process.env['ProgramFiles(x86)'] ? [`${process.env['ProgramFiles(x86)']}\\Wireshark\\tshark.exe`] : [])
46 | ];
47 |
48 | case 'darwin':
49 | return [
50 | '/opt/homebrew/bin/tshark',
51 | '/usr/local/bin/tshark',
52 | '/Applications/Wireshark.app/Contents/MacOS/tshark',
53 | '/usr/bin/tshark'
54 | ];
55 |
56 | case 'linux':
57 | return [
58 | '/usr/bin/tshark',
59 | '/usr/local/bin/tshark',
60 | '/snap/bin/tshark',
61 | '/usr/sbin/tshark'
62 | ];
63 |
64 | default:
65 | return ['/usr/bin/tshark', '/usr/local/bin/tshark'];
66 | }
67 | };
68 |
69 | // Try fallback paths
70 | const fallbackPaths = getFallbackPaths();
71 | for (const candidatePath of fallbackPaths) {
72 | try {
73 | await execAsync(`"${candidatePath}" -v`, { timeout: 5000 });
74 | console.error(`Found tshark at fallback: ${candidatePath}`);
75 | return candidatePath;
76 | } catch {
77 | // Continue to next fallback
78 | }
79 | }
80 |
81 | throw new Error(
82 | 'tshark not found. Please install Wireshark (https://www.wireshark.org/download.html) and ensure tshark is in your PATH.'
83 | );
84 | }
85 |
86 | /**
87 | * Load config file, creating default if it doesn't exist
88 | */
89 | export async function loadConfigFile(): Promise<ConfigFile> {
90 | try {
91 | const configContent = await fs.readFile(CONFIG_FILE_PATH, 'utf8');
92 | return JSON.parse(configContent);
93 | } catch (error) {
94 | // Create default config file if it doesn't exist
95 | const defaultConfig: ConfigFile = {
96 | version: "0.1.0",
97 | configs: {}
98 | };
99 | await saveConfigFile(defaultConfig);
100 | return defaultConfig;
101 | }
102 | }
103 |
104 | /**
105 | * Save config file
106 | */
107 | export async function saveConfigFile(config: ConfigFile): Promise<void> {
108 | await fs.writeFile(CONFIG_FILE_PATH, JSON.stringify(config, null, 2));
109 | }
110 |
111 | /**
112 | * Save a filter configuration
113 | */
114 | export async function saveFilterConfig(filterConfig: FilterConfig): Promise<void> {
115 | const configFile = await loadConfigFile();
116 | configFile.configs[filterConfig.name] = filterConfig;
117 | await saveConfigFile(configFile);
118 | }
119 |
120 | /**
121 | * Load a filter configuration by name
122 | */
123 | export async function loadFilterConfig(name: string): Promise<FilterConfig | null> {
124 | const configFile = await loadConfigFile();
125 | return configFile.configs[name] || null;
126 | }
127 |
128 | /**
129 | * List all available filter configurations
130 | */
131 | export async function listFilterConfigs(): Promise<FilterConfig[]> {
132 | const configFile = await loadConfigFile();
133 | return Object.values(configFile.configs);
134 | }
135 |
136 | /**
137 | * Delete a filter configuration
138 | */
139 | export async function deleteFilterConfig(name: string): Promise<boolean> {
140 | const configFile = await loadConfigFile();
141 | if (configFile.configs[name]) {
142 | delete configFile.configs[name];
143 | await saveConfigFile(configFile);
144 | return true;
145 | }
146 | return false;
147 | }
148 |
149 | /**
150 | * Process tshark output based on format
151 | */
152 | export function processTsharkOutput(
153 | stdout: string,
154 | outputFormat: OutputFormat
155 | ): string {
156 | switch (outputFormat) {
157 | case 'json':
158 | // Try to parse and format JSON for readability
159 | try {
160 | const parsed = JSON.parse(stdout);
161 | return JSON.stringify(parsed, null, 2);
162 | } catch {
163 | return stdout; // Return raw if parsing fails
164 | }
165 | case 'fields':
166 | case 'text':
167 | default:
168 | return stdout; // Return raw output
169 | }
170 | }
171 |
172 | /**
173 | * Reusable function for PCAP analysis with comprehensive cross-platform error handling
174 | */
175 | export async function analyzePcap(
176 | filePath: string,
177 | displayFilter: string = '',
178 | outputFormat: OutputFormat = 'text',
179 | customFields?: string,
180 | sslKeylogFile?: string
181 | ): Promise<string> {
182 | const tsharkPath = await findTshark();
183 |
184 | // Set up SSL keylog for decryption during analysis
185 | const analysisEnv: TsharkEnvironment = Object.fromEntries(
186 | Object.entries(process.env).filter(([_, value]) => value !== undefined)
187 | ) as TsharkEnvironment;
188 |
189 | const keylogToUse = sslKeylogFile || process.env.SSLKEYLOGFILE;
190 | if (keylogToUse) {
191 | console.error(`Using SSL keylog file for decryption: ${keylogToUse}`);
192 | analysisEnv.SSLKEYLOGFILE = keylogToUse;
193 | }
194 |
195 | // Build command based on output format using absolute tshark path
196 | let command: string;
197 | const sslOptions = keylogToUse ? `-o tls.keylog_file:"${keylogToUse}"` : '';
198 | const filterOption = displayFilter ? `-Y "${displayFilter}"` : '';
199 | const quotedTsharkPath = `"${tsharkPath}"`;
200 |
201 | switch (outputFormat) {
202 | case 'json':
203 | command = `${quotedTsharkPath} -r "${filePath}" ${sslOptions} ${filterOption} -T json`;
204 | break;
205 | case 'fields':
206 | const fieldsToUse = customFields || 'frame.number,frame.time_relative,ip.src,ip.dst,tcp.srcport,tcp.dstport';
207 | const fieldArgs = fieldsToUse.split(',').map(field => `-e ${field.trim()}`).join(' ');
208 | command = `${quotedTsharkPath} -r "${filePath}" ${sslOptions} ${filterOption} -T fields ${fieldArgs}`;
209 | break;
210 | case 'text':
211 | default:
212 | command = `${quotedTsharkPath} -r "${filePath}" ${sslOptions} ${filterOption}`;
213 | break;
214 | }
215 |
216 | // Execution options with increased buffer
217 | const execOptions = {
218 | env: analysisEnv,
219 | maxBuffer: 50 * 1024 * 1024 // 50MB buffer
220 | };
221 |
222 | console.error(`Analyzing capture with command: ${command}`);
223 | const { stdout } = await execAsync(command, execOptions);
224 | return processTsharkOutput(stdout, outputFormat);
225 | }
226 |
227 | /**
228 | * Trim output if it exceeds maximum character limits
229 | * Different formats have different optimal limits for readability
230 | */
231 | export function trimOutput(output: string, outputFormat: OutputFormat): string {
232 | // Format-specific limits for optimal readability
233 | const maxChars = outputFormat === 'json' ? 500000 :
234 | outputFormat === 'fields' ? 800000 :
235 | 720000; // text format default
236 |
237 | if (output.length > maxChars) {
238 | const trimPoint = maxChars - 500;
239 | const formatInfo = outputFormat !== 'text' ? ` (${outputFormat} format)` : '';
240 | const trimmed = output.substring(0, trimPoint) + `\n\n... [Output truncated due to size${formatInfo}] ...`;
241 | console.error(`Trimmed ${outputFormat} output from ${output.length} to ${maxChars} chars`);
242 | return trimmed;
243 | }
244 | return output;
245 | }
```
--------------------------------------------------------------------------------
/test/integration.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Integration Tests for SharkMCP Server
3 | * Tests the full MCP server functionality using the SDK client
4 | */
5 |
6 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8 | import { spawn } from "child_process";
9 | import { fileURLToPath } from "url";
10 | import { dirname, join } from "path";
11 | import process from "process";
12 |
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = dirname(__filename);
15 | const projectRoot = join(__dirname, '..');
16 |
17 | /**
18 | * Test suite configuration
19 | */
20 | const TEST_CONFIG = {
21 | serverPath: join(projectRoot, 'dist', 'index.js'),
22 | testInterface: process.platform === 'darwin' ? 'en0' : 'eth0', // Adjust based on platform
23 | captureTimeout: 12, // Slightly longer than config timeout to ensure completion
24 | configName: 'integration_test'
25 | };
26 |
27 | /**
28 | * Utility function to wait for a specified time
29 | */
30 | function sleep(ms) {
31 | return new Promise(resolve => setTimeout(resolve, ms));
32 | }
33 |
34 | /**
35 | * Generate some network traffic to ensure we capture packets
36 | */
37 | async function generateNetworkTraffic() {
38 | console.log('Generating network traffic...');
39 |
40 | // Create multiple concurrent network requests to generate traffic
41 | const trafficPromises = [
42 | // HTTP requests
43 | fetch('http://httpbin.org/get').catch(() => {}),
44 | fetch('http://example.com').catch(() => {}),
45 | // DNS lookups via fetch will generate UDP traffic
46 | fetch('http://google.com').catch(() => {}),
47 | fetch('http://github.com').catch(() => {}),
48 | ];
49 |
50 | // Don't wait for all to complete, just start them
51 | await Promise.allSettled(trafficPromises.slice(0, 2)); // Wait for first 2
52 | console.log('Network traffic generated');
53 | }
54 |
55 | /**
56 | * Extract packet count from tshark JSON output
57 | */
58 | function countPacketsFromOutput(output, outputFormat) {
59 | if (!output || output.trim() === '') {
60 | return 0;
61 | }
62 |
63 | try {
64 | switch (outputFormat) {
65 | case 'json':
66 | // For JSON format, parse and count array elements
67 | const parsed = JSON.parse(output);
68 | if (Array.isArray(parsed)) {
69 | return parsed.length;
70 | } else if (parsed._source) {
71 | // Single packet format
72 | return 1;
73 | }
74 | return 0;
75 |
76 | case 'fields':
77 | // For fields format, count non-empty lines
78 | return output.split('\n').filter(line => line.trim().length > 0).length;
79 |
80 | case 'text':
81 | default:
82 | // For text format, count lines that look like packet headers
83 | const lines = output.split('\n');
84 | return lines.filter(line =>
85 | line.match(/^\s*\d+\s+\d+\.\d+/) || // Standard packet line
86 | line.includes('Ethernet') ||
87 | line.includes('Internet Protocol')
88 | ).length;
89 | }
90 | } catch (error) {
91 | console.warn(`Warning: Failed to parse output for packet counting: ${error.message}`);
92 | // Fallback: count non-empty lines
93 | return output.split('\n').filter(line => line.trim().length > 0).length;
94 | }
95 | }
96 |
97 | /**
98 | * Main integration test runner
99 | */
100 | async function runIntegrationTests() {
101 | console.log('Starting SharkMCP Integration Tests');
102 | console.log(`Project root: ${projectRoot}`);
103 | console.log(`Server path: ${TEST_CONFIG.serverPath}`);
104 | console.log(`Test interface: ${TEST_CONFIG.testInterface}`);
105 |
106 | let client;
107 | let transport;
108 |
109 | try {
110 | // Initialize MCP client with server transport
111 | console.log('\nSetting up MCP client transport...');
112 | transport = new StdioClientTransport({
113 | command: "node",
114 | args: [TEST_CONFIG.serverPath]
115 | });
116 |
117 | client = new Client({
118 | name: "sharkmcp-integration-test",
119 | version: "1.0.0"
120 | });
121 |
122 | console.log('Connecting to MCP server...');
123 | await client.connect(transport);
124 | console.log('Successfully connected to MCP server');
125 |
126 | // Test 1: List available tools
127 | console.log('\nTest 1: Listing available tools...');
128 | const tools = await client.listTools();
129 | console.log(`Found ${tools.tools.length} tools:`);
130 | tools.tools.forEach(tool => {
131 | console.log(` - ${tool.name}: ${tool.description}`);
132 | });
133 |
134 | const expectedTools = ['start_capture_session', 'stop_capture_session', 'analyze_pcap_file', 'manage_config'];
135 | const foundTools = tools.tools.map(t => t.name);
136 | const missingTools = expectedTools.filter(tool => !foundTools.includes(tool));
137 |
138 | if (missingTools.length > 0) {
139 | throw new Error(`Missing expected tools: ${missingTools.join(', ')}`);
140 | }
141 | console.log('All expected tools found');
142 |
143 | // Test 2: Load and verify test configuration
144 | console.log('\nTest 2: Loading test configuration...');
145 | const configResult = await client.callTool({
146 | name: "manage_config",
147 | arguments: {
148 | action: "load",
149 | name: TEST_CONFIG.configName
150 | }
151 | });
152 |
153 | if (configResult.isError) {
154 | throw new Error(`Failed to load test config: ${configResult.content[0].text}`);
155 | }
156 | console.log('Test configuration loaded successfully');
157 | console.log(configResult.content[0].text);
158 |
159 | // Test 3: Start capture session using saved config
160 | console.log('\nTest 3: Starting packet capture session...');
161 | const startResult = await client.callTool({
162 | name: "start_capture_session",
163 | arguments: {
164 | configName: TEST_CONFIG.configName,
165 | interface: TEST_CONFIG.testInterface
166 | }
167 | });
168 |
169 | if (startResult.isError) {
170 | throw new Error(`Failed to start capture: ${startResult.content[0].text}`);
171 | }
172 |
173 | const startText = startResult.content[0].text;
174 | console.log('Capture session started');
175 | console.log(startText);
176 |
177 | // Extract session ID from response
178 | const sessionIdMatch = startText.match(/Session ID: ([\w_]+)/);
179 | if (!sessionIdMatch) {
180 | throw new Error('Could not extract session ID from start response');
181 | }
182 | const sessionId = sessionIdMatch[1];
183 | console.log(`Session ID: ${sessionId}`);
184 |
185 | // Test 4: Generate network traffic during capture
186 | console.log('\nTest 4: Generating network traffic...');
187 | await sleep(2000); // Wait 2 seconds after starting capture
188 | await generateNetworkTraffic();
189 |
190 | // Wait for remaining capture time
191 | const remainingTime = (TEST_CONFIG.captureTimeout - 3) * 1000; // 3 seconds already passed
192 | console.log(`Waiting ${remainingTime/1000}s for capture to complete...`);
193 | await sleep(remainingTime);
194 |
195 | // Test 5: Stop capture and analyze results
196 | console.log('\nTest 5: Stopping capture and analyzing results...');
197 | const stopResult = await client.callTool({
198 | name: "stop_capture_session",
199 | arguments: {
200 | sessionId: sessionId,
201 | outputFormat: "json"
202 | }
203 | });
204 |
205 | if (stopResult.isError) {
206 | throw new Error(`Failed to stop capture: ${stopResult.content[0].text}`);
207 | }
208 |
209 | const stopText = stopResult.content[0].text;
210 | console.log('Capture session stopped and analyzed');
211 |
212 | // Test 6: Extract and count packets
213 | console.log('\nTest 6: Counting captured packets...');
214 |
215 | // Extract the JSON results section
216 | const resultsMatch = stopText.match(/Packet Analysis Results:\n(.*)/s);
217 | if (!resultsMatch) {
218 | console.warn('Could not extract packet analysis results from response');
219 | console.log('Full response:');
220 | console.log(stopText);
221 | } else {
222 | const packetData = resultsMatch[1];
223 | const packetCount = countPacketsFromOutput(packetData, 'json');
224 |
225 | console.log(`Packet count: ${packetCount}`);
226 |
227 | if (packetCount === 0) {
228 | console.warn('No packets captured - this could indicate:');
229 | console.warn(' - No network traffic on interface during capture');
230 | console.warn(' - Interface name incorrect for this system');
231 | console.warn(' - Permission issues with packet capture');
232 | console.warn(' - tshark not working properly');
233 | } else {
234 | console.log(`Successfully captured ${packetCount} packets`);
235 | }
236 |
237 | // Show some sample output
238 | if (packetData.length > 0) {
239 | const sampleLength = Math.min(500, packetData.length);
240 | console.log('\nSample output (first 500 chars):');
241 | console.log(packetData.substring(0, sampleLength));
242 | if (packetData.length > sampleLength) {
243 | console.log('... (truncated)');
244 | }
245 | }
246 | }
247 |
248 | // Test 7: Test PCAP file analysis (if we have the test file)
249 | console.log('\nTest 7: Testing PCAP file analysis...');
250 | try {
251 | const pcapResult = await client.callTool({
252 | name: "analyze_pcap_file",
253 | arguments: {
254 | filePath: join(projectRoot, 'test', 'dump.pcapng'),
255 | outputFormat: "json",
256 | displayFilter: ""
257 | }
258 | });
259 |
260 | if (!pcapResult.isError) {
261 | const pcapText = pcapResult.content[0].text;
262 | const pcapResultsMatch = pcapText.match(/Packet Analysis Results:\n(.*)/s);
263 |
264 | if (pcapResultsMatch) {
265 | const pcapPacketData = pcapResultsMatch[1];
266 | const pcapPacketCount = countPacketsFromOutput(pcapPacketData, 'json');
267 | console.log(`PCAP file analysis successful: ${pcapPacketCount} packets found`);
268 | } else {
269 | console.log('PCAP file analysis completed (format parsing issue)');
270 | }
271 | } else {
272 | console.log('PCAP file analysis failed (test file may not exist)');
273 | }
274 | } catch (error) {
275 | console.log(`PCAP file analysis test skipped: ${error.message}`);
276 | }
277 |
278 | // Test 8: Test TLS handshake filtering on dump.pcapng
279 | console.log('\nTest 8: Testing TLS handshake filter on dump.pcapng...');
280 | try {
281 | const tlsResult = await client.callTool({
282 | name: "analyze_pcap_file",
283 | arguments: {
284 | filePath: join(projectRoot, 'test', 'dump.pcapng'),
285 | outputFormat: "json",
286 | displayFilter: "tls.handshake.type == 1"
287 | }
288 | });
289 |
290 | if (!tlsResult.isError) {
291 | const tlsText = tlsResult.content[0].text;
292 | const tlsResultsMatch = tlsText.match(/Packet Analysis Results:\n(.*)/s);
293 |
294 | if (tlsResultsMatch) {
295 | const tlsPacketData = tlsResultsMatch[1];
296 | const tlsPacketCount = countPacketsFromOutput(tlsPacketData, 'json');
297 |
298 | if (tlsPacketCount > 0) {
299 | console.log(`TLS handshake filter successful: Found ${tlsPacketCount} TLS Client Hello packets`);
300 |
301 | // Show a sample of the TLS handshake data
302 | if (tlsPacketData.length > 0) {
303 | const sampleLength = Math.min(300, tlsPacketData.length);
304 | console.log('\nSample TLS handshake data (first 300 chars):');
305 | console.log(tlsPacketData.substring(0, sampleLength));
306 | if (tlsPacketData.length > sampleLength) {
307 | console.log('... (truncated)');
308 | }
309 | }
310 | } else {
311 | console.log('TLS handshake filter returned no packets - dump.pcapng may not contain TLS Client Hello packets');
312 | }
313 | } else {
314 | console.log('TLS handshake filter completed but could not parse results');
315 | }
316 | } else {
317 | console.log(`TLS handshake filter failed: ${tlsResult.content[0].text}`);
318 | }
319 | } catch (error) {
320 | console.log(`TLS handshake filter test failed: ${error.message}`);
321 | }
322 |
323 | console.log('\nIntegration tests completed successfully!');
324 | console.log('\nTest Summary:');
325 | console.log('- MCP server connection and communication');
326 | console.log('- Tool discovery and listing');
327 | console.log('- Configuration management');
328 | console.log('- Packet capture session lifecycle');
329 | console.log('- Network traffic generation and capture');
330 | console.log('- Packet analysis and counting');
331 | console.log('- PCAP file analysis with display filters');
332 | console.log('- TLS handshake packet filtering');
333 | console.log('- Error handling and edge cases');
334 |
335 | return true;
336 |
337 | } catch (error) {
338 | console.error('\nIntegration test failed:');
339 | console.error(error.message);
340 | console.error('\nStack trace:');
341 | console.error(error.stack);
342 | return false;
343 |
344 | } finally {
345 | // Clean up
346 | if (client && transport) {
347 | try {
348 | console.log('\nCleaning up MCP connection...');
349 | await client.close();
350 | console.log('MCP connection closed');
351 | } catch (error) {
352 | console.warn(`Warning during cleanup: ${error.message}`);
353 | }
354 | }
355 | }
356 | }
357 |
358 | // Run tests if this file is executed directly
359 | if (import.meta.url === `file://${process.argv[1]}`) {
360 | const success = await runIntegrationTests();
361 | process.exit(success ? 0 : 1);
362 | }
363 |
364 | export { runIntegrationTests };
```