#
tokens: 15488/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 }; 
```