#
tokens: 11816/50000 12/12 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
# Dependencies
node_modules/
npm-debug.log*
pnpm-debug.log*

# Build outputs
dist/
build/
*.tsbuildinfo

*.log
*.pid
*.seed
*.lock
.DS_Store
Thumbs.db

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db

/captures/
sslkeylog.log

# sharkmcp-configs.json
```

--------------------------------------------------------------------------------
/sharkmcp-configs.json:
--------------------------------------------------------------------------------

```json
{
  "version": "1.0.0",
  "configs": {
    "tls_analysis": {
      "name": "tls_analysis",
      "description": "TLS traffic analysis with handshake details",
      "captureFilter": "port 443",
      "displayFilter": "tls.handshake",
      "outputFormat": "json",
      "maxPackets": 500,
      "interface": "en0"
    },
    "integration_test": {
      "name": "integration_test",
      "description": "Short capture for integration testing",
      "captureFilter": "",
      "displayFilter": "",
      "outputFormat": "json",
      "maxPackets": 100,
      "timeout": 10,
      "interface": "en0"
    }
  }
}
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": false,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "allowUnusedLabels": false,
    "allowUnreachableCode": false,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": false
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "test"
  ]
} 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "SharkMCP",
  "version": "0.1.0",
  "description": "A Wireshark MCP server for network packet analysis",
  "main": "src/index.ts",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "dev": "node --loader ts-node/esm src/index.ts",
    "start": "node dist/index.js",
    "test": "pnpm run test:integration",
    "test:unit": "echo 'Unit tests not yet implemented'",
    "test:integration": "pnpm run build && node test/integration.test.js",
    "test:direct": "pnpm run build && node test-client.js"
  },
  "keywords": [
    "sharkmcp",
    "wireshark",
    "mcp",
    "network",
    "security",
    "packet-analysis",
    "tshark"
  ],
  "author": "",
  "license": "ISC",
  "packageManager": "[email protected]",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.1",
    "@types/node": "24.0.0",
    "axios": "1.9.0",
    "which": "5.0.0",
    "zod": "^3.25.61"
  },
  "devDependencies": {
    "@types/which": "^3.0.4",
    "ts-node": "^10.9.2",
    "tsx": "^4.20.1",
    "typescript": "^5.8.3"
  }
}

```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
import { ChildProcess } from "child_process";

/**
 * Represents an active packet capture session
 */
export interface CaptureSession {
  id: string;
  process: ChildProcess | null;
  interface: string;
  captureFilter?: string;
  timeout: number;
  maxPackets: number;
  startTime: Date;
  tempFile: string;
  status: 'running' | 'completed' | 'error';
  endTime?: Date;
  exitCode?: number;
}

/**
 * Output format options for packet analysis
 */
export type OutputFormat = 'json' | 'fields' | 'text';

/**
 * Environment configuration for tshark processes
 */
export interface TsharkEnvironment {
  [key: string]: string;
}

/**
 * Configuration for PCAP analysis
 */
export interface AnalysisConfig {
  filePath: string;
  displayFilter: string;
  outputFormat: OutputFormat;
  customFields?: string;
  sslKeylogFile?: string;
}

/**
 * Reusable filter configuration that LLMs can save and reuse
 */
export interface FilterConfig {
  name: string;
  description?: string;
  captureFilter?: string;
  displayFilter?: string;
  outputFormat?: OutputFormat;
  customFields?: string;
  timeout?: number;
  maxPackets?: number;
  interface?: string;
}

/**
 * Config file structure
 */
export interface ConfigFile {
  version: string;
  configs: { [name: string]: FilterConfig };
} 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CaptureSession } from "./types.js";
import { startCaptureSessionSchema, startCaptureSessionHandler } from "./tools/start-capture-session.js";
import { stopCaptureSessionSchema, stopCaptureSessionHandler } from "./tools/stop-capture-session.js";
import { analyzePcapFileSchema, analyzePcapFileHandler } from "./tools/analyze-pcap-file.js";
import { manageConfigSchema, manageConfigHandler } from "./tools/manage-config.js";

// Active capture sessions storage
const activeSessions = new Map<string, CaptureSession>();

// Initialize MCP server
const server = new McpServer({
  name: 'SharkMCP',
  version: '0.1.0',
});

/**
 * Register all tools with the MCP server
 * Each tool is defined in its own module for better organization
 */

// Tool 1: Start background packet capture session
server.tool(
  'start_capture_session',
  'Start a background packet capture session. LLMs control all capture parameters including filters, interfaces, and packet limits. Can use saved configurations.',
  startCaptureSessionSchema,
  async (args) => startCaptureSessionHandler(args, activeSessions)
);

// Tool 2: Stop capture session and retrieve results
server.tool(
  'stop_capture_session',
  'Stop a running capture session and analyze packets. LLMs control all analysis parameters including display filters and output formats. Can use saved configurations.',
  stopCaptureSessionSchema,
  async (args) => stopCaptureSessionHandler(args, activeSessions)
);

// Tool 3: Analyze an existing PCAP file
server.tool(
  'analyze_pcap_file',
  'Analyze a local pcap/pcapng file. LLMs control all analysis parameters including filters, output formats, and custom fields. Can use saved configurations.',
  analyzePcapFileSchema,
  async (args) => analyzePcapFileHandler(args)
);

// Tool 4: Manage filter configurations
server.tool(
  'manage_config',
  'Save, load, list, or delete reusable filter configurations. Allows LLMs to store commonly used capture and analysis parameters for easy reuse.',
  manageConfigSchema,
  async (args) => manageConfigHandler(args)
);

// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);

console.error("SharkMCP server is running and connected to transport. Ready for requests.");
```

--------------------------------------------------------------------------------
/src/tools/analyze-pcap-file.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import fs from "fs/promises";
import { analyzePcap, trimOutput, loadFilterConfig } from "../utils.js";

/**
 * Input schema for analyze pcap file tool
 */
export const analyzePcapFileSchema = {
  filePath: z.string().describe('Path to the local .pcap or .pcapng file to analyze.'),
  displayFilter: z.string().optional().describe('Wireshark display filter for analysis (e.g., "tls.handshake.type == 1")'),
  outputFormat: z.enum(['json', 'fields', 'text']).optional().default('text').describe('Output format: json (-T json), fields (custom -e), or text (default wireshark output)'),
  customFields: z.string().optional().describe('Custom tshark field list (only used with outputFormat=fields)'),
  sslKeylogFile: z.string().optional().describe('ABSOLUTE path to SSL keylog file for TLS decryption'),
  configName: z.string().optional().describe('Name of saved configuration to use for analysis parameters')
};

/**
 * Tool handler for analyzing an existing PCAP file
 * This tool analyzes pre-existing PCAP/PCAPNG files without needing to capture
 */
export async function analyzePcapFileHandler(args: any) {
  try {
    let { filePath, displayFilter, outputFormat, customFields, sslKeylogFile, configName } = args;

    // If configName is provided, load and use that configuration for analysis
    if (configName) {
      const savedConfig = await loadFilterConfig(configName);
      if (!savedConfig) {
        return {
          content: [{
            type: 'text' as const,
            text: `Error: Configuration '${configName}' not found. Use manage_config with action 'list' to see available configurations.`,
          }],
          isError: true
        };
      }
      
      // Override analysis parameters with saved config (saved config takes precedence)
      if (savedConfig.displayFilter) displayFilter = savedConfig.displayFilter;
      if (savedConfig.outputFormat) outputFormat = savedConfig.outputFormat;
      if (savedConfig.customFields) customFields = savedConfig.customFields;
      
      console.error(`Using saved configuration '${configName}' for analysis: ${JSON.stringify(savedConfig)}`);
    }

    // Verify file exists before proceeding
    await fs.access(filePath);

    // Analyze the file using the reusable function
    const output = await analyzePcap(
      filePath,
      displayFilter,
      outputFormat,
      customFields,
      sslKeylogFile
    );

    const keylogToUse = sslKeylogFile || process.env.SSLKEYLOGFILE;

    // Trim output if too large
    const trimmedOutput = trimOutput(output, outputFormat);

    const configInfo = configName ? `\nUsing saved config: ${configName}` : '';
    
    return {
      content: [{
        type: 'text' as const,
        text: `Analysis of '${filePath}' complete!${configInfo}\nDisplay Filter: ${displayFilter || 'none'}\nOutput Format: ${outputFormat}\nSSL Decryption: ${keylogToUse ? 'Enabled' : 'Disabled'}\n\nPacket Analysis Results:\n${trimmedOutput}`,
      }],
    };
  } catch (error: any) {
    console.error(`Error analyzing PCAP file: ${error.message}`);
    return { 
      content: [{ type: 'text' as const, text: `Error: ${error.message}` }], 
      isError: true 
    };
  }
} 
```

--------------------------------------------------------------------------------
/src/tools/stop-capture-session.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import fs from "fs/promises";
import { CaptureSession } from "../types.js";
import { analyzePcap, trimOutput, loadFilterConfig } from "../utils.js";

/**
 * Input schema for stop capture session tool
 */
export const stopCaptureSessionSchema = {
  sessionId: z.string().describe('Session ID returned from start_capture_session'),
  displayFilter: z.string().optional().describe('Wireshark display filter for analysis (e.g., "tls.handshake.type == 1")'),
  outputFormat: z.enum(['json', 'fields', 'text']).optional().default('text').describe('Output format: json (-T json), fields (custom -e), or text (default wireshark output)'),
  customFields: z.string().optional().describe('Custom tshark field list (only used with outputFormat=fields)'),
  sslKeylogFile: z.string().optional().describe('ABSOLUTE path to SSL keylog file for TLS decryption'),
  configName: z.string().optional().describe('Name of saved configuration to use for analysis parameters')
};

/**
 * Tool handler for stopping capture session and retrieving results
 * This tool stops a running capture session and analyzes the captured packets
 */
export async function stopCaptureSessionHandler(args: any, activeSessions: Map<string, CaptureSession>) {
  try {
    let { sessionId, displayFilter, outputFormat, customFields, sslKeylogFile, configName } = args;
    const session = activeSessions.get(sessionId);
    
    if (!session) {
      return {
        content: [{
          type: 'text' as const,
          text: `Error: No active session found with ID '${sessionId}'. Use 'list_capture_sessions' to see active sessions.`,
        }],
        isError: true
      };
    }

    // If configName is provided, load and use that configuration for analysis
    if (configName) {
      const savedConfig = await loadFilterConfig(configName);
      if (!savedConfig) {
        return {
          content: [{
            type: 'text' as const,
            text: `Error: Configuration '${configName}' not found. Use manage_config with action 'list' to see available configurations.`,
          }],
          isError: true
        };
      }
      
      // Override analysis parameters with saved config (saved config takes precedence)
      if (savedConfig.displayFilter) displayFilter = savedConfig.displayFilter;
      if (savedConfig.outputFormat) outputFormat = savedConfig.outputFormat;
      if (savedConfig.customFields) customFields = savedConfig.customFields;
      
      console.error(`Using saved configuration '${configName}' for analysis: ${JSON.stringify(savedConfig)}`);
    }

    console.error(`Stopping capture session: ${sessionId}`);

    // Check if the capture process has already completed naturally
    if (session.process && !session.process.killed && session.status === 'running') {
      console.error(`Terminating capture process for session ${sessionId}`);
      session.process.kill('SIGTERM');
      // Wait a moment for graceful termination
      await new Promise(resolve => setTimeout(resolve, 2000));
    } else if (session.status === 'completed') {
      console.error(`Capture session ${sessionId} already completed naturally`);
    } else {
      console.error(`Capture session ${sessionId} process already terminated`);
    }

    // Remove from active sessions
    activeSessions.delete(sessionId);

    try {
      // Check if file exists
      await fs.access(session.tempFile);
      
      // Wait a bit more to ensure file is fully written
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      // Analyze captured file using the reusable function
      const output = await analyzePcap(
        session.tempFile,
        displayFilter,
        outputFormat,
        customFields,
        sslKeylogFile
      );
      
      const keylogToUse = sslKeylogFile || process.env.SSLKEYLOGFILE;

      // Clean up temporary file
      await fs.unlink(session.tempFile).catch(err => 
        console.error(`Failed to delete ${session.tempFile}: ${err.message}`)
      );

      const duration = new Date().getTime() - session.startTime.getTime();
      const durationSec = (duration / 1000).toFixed(1);

      // Trim output if too large
      const trimmedOutput = trimOutput(output, outputFormat);

      const configInfo = configName ? `\nUsing saved config: ${configName}` : '';

      return {
        content: [{
          type: 'text' as const,
          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}`,
        }],
      };

    } catch (fileError: any) {
      console.error(`Error analyzing session ${sessionId}: ${fileError.message}`);
      return {
        content: [{
          type: 'text' as const,
          text: `Error analyzing session '${sessionId}': Capture file not found or unreadable. This could mean no packets were captured.\nDetails: ${fileError.message}`,
        }],
        isError: true,
      };
    }
  } catch (error: any) {
    console.error(`Error stopping capture session: ${error.message}`);
    return { 
      content: [{ type: 'text' as const, text: `Error: ${error.message}` }], 
      isError: true 
    };
  }
} 
```

--------------------------------------------------------------------------------
/src/tools/start-capture-session.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { spawn } from "child_process";
import { CaptureSession } from "../types.js";
import { findTshark, loadFilterConfig } from "../utils.js";

/**
 * Input schema for start capture session tool
 */
export const startCaptureSessionSchema = {
  interface: z.string().optional().default('lo0').describe('Network interface to capture from (e.g., eth0, en0, lo0)'),
  captureFilter: z.string().optional().describe('Optional BPF capture filter to apply while capturing (e.g., "port 443")'),
  timeout: z.number().optional().default(60).describe('Timeout in seconds before auto-stopping capture (default: 60s to prevent orphaned sessions)'),
  maxPackets: z.number().optional().default(100000).describe('Maximum number of packets to capture (safety limit, default: 100,000)'),
  sessionName: z.string().optional().describe('Optional session name for easier identification'),
  configName: z.string().optional().describe('Name of saved configuration to use (will override other parameters)')
};

/**
 * Tool handler for starting background packet capture session
 * This tool starts a detached tshark process to capture network packets
 */
export async function startCaptureSessionHandler(args: any, activeSessions: Map<string, CaptureSession>) {
  try {
    const tsharkPath = await findTshark();
    let { interface: networkInterface, captureFilter, timeout, maxPackets, sessionName, configName } = args;
    
    // If configName is provided, load and use that configuration
    if (configName) {
      const savedConfig = await loadFilterConfig(configName);
      if (!savedConfig) {
        return {
          content: [{
            type: 'text' as const,
            text: `Error: Configuration '${configName}' not found. Use manage_config with action 'list' to see available configurations.`,
          }],
          isError: true
        };
      }
      
      // Override parameters with saved config (saved config takes precedence)
      if (savedConfig.interface) networkInterface = savedConfig.interface;
      if (savedConfig.captureFilter) captureFilter = savedConfig.captureFilter;
      if (savedConfig.timeout) timeout = savedConfig.timeout;
      if (savedConfig.maxPackets) maxPackets = savedConfig.maxPackets;
      
      console.error(`Using saved configuration '${configName}': ${JSON.stringify(savedConfig)}`);
    }
    
    // Generate unique session ID
    const sessionId = sessionName || `capture_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    
    // Check if session already exists
    if (activeSessions.has(sessionId)) {
      return {
        content: [{
          type: 'text' as const,
          text: `Error: Session '${sessionId}' already exists. Use a different session name or stop the existing session.`,
        }],
        isError: true
      };
    }

    const tempFile = `/tmp/shark_${sessionId}.pcap`;
    console.error(`Starting capture session: ${sessionId} on ${networkInterface}`);

    // Build comprehensive tshark command for background capture
    // Use timeout as primary stopping mechanism, with maxPackets as safety limit
    const args_array = [
      '-i', networkInterface,
      '-a', `duration:${timeout}`,  // Auto-stop after timeout seconds
      '-c', maxPackets.toString(),  // Safety limit to prevent excessive capture
      '-w', tempFile
    ];
    
    // Add capture filter if provided (as a single argument to -f)
    if (captureFilter) {
      args_array.push('-f', captureFilter);
    }

    // Set up basic environment
    const captureEnv: Record<string, string> = {
      ...process.env,
      PATH: `${process.env.PATH}:/usr/bin:/usr/local/bin:/opt/homebrew/bin`
    };
    
    // Log the command with proper quoting for copy-paste debugging
    const quotedArgs = args_array.map(arg => {
      // Quote arguments that contain spaces or special characters
      if (arg.includes(' ') || arg.includes('|') || arg.includes('&') || arg.includes('(') || arg.includes(')')) {
        return `"${arg}"`;
      }
      return arg;
    });
    console.error(`Running background command: ${tsharkPath} ${quotedArgs.join(' ')}`);

    // Start background capture process with stderr logging
    const captureProcess = spawn(tsharkPath, args_array, {
      env: captureEnv,
      stdio: ['ignore', 'ignore', 'pipe'], // Capture stderr for error logging
      detached: true   // Fully detach the process
    });
    
    // Log any errors from tshark
    if (captureProcess.stderr) {
      captureProcess.stderr.on('data', (data) => {
        console.error(`tshark stderr [${sessionId}]: ${data.toString().trim()}`);
      });
    }
    
    // Unref the process so the parent can exit independently
    captureProcess.unref();

    // Store session info
    const session: CaptureSession = {
      id: sessionId,
      process: captureProcess,
      interface: networkInterface,
      captureFilter,
      timeout,
      maxPackets,
      startTime: new Date(),
      tempFile,
      status: 'running'
    };
    
    activeSessions.set(sessionId, session);

    // Handle process completion - KEEP SESSION ALIVE for result retrieval
    captureProcess.on('exit', (code) => {
      console.error(`Capture session ${sessionId} exited with code: ${code}`);
      if (activeSessions.has(sessionId)) {
        const session = activeSessions.get(sessionId)!;
        session.process = null;
        session.status = code === 0 ? 'completed' : 'error';
        session.endTime = new Date();
        if (code !== null) {
          session.exitCode = code;
        }
        console.error(`Session ${sessionId} marked as ${session.status}, file: ${session.tempFile}`);
      }
    });

    captureProcess.on('error', (error) => {
      console.error(`Capture session ${sessionId} error: ${error.message}`);
      if (activeSessions.has(sessionId)) {
        const session = activeSessions.get(sessionId)!;
        session.process = null;
        session.status = 'error';
        session.endTime = new Date();
      }
    });

    const configInfo = configName ? `\nUsing saved config: ${configName}` : '';
    
    return {
      content: [{
        type: 'text' as const,
        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.`,
      }],
    };
  } catch (error: any) {
    console.error(`Error starting capture session: ${error.message}`);
    return { 
      content: [{ type: 'text' as const, text: `Error: ${error.message}` }], 
      isError: true 
    };
  }
} 
```

--------------------------------------------------------------------------------
/src/tools/manage-config.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";
import { FilterConfig } from "../types.js";
import { 
  saveFilterConfig, 
  loadFilterConfig, 
  listFilterConfigs, 
  deleteFilterConfig 
} from "../utils.js";

/**
 * Input schema for config management tool
 */
export const manageConfigSchema = {
  action: z.enum(['save', 'load', 'list', 'view', 'delete']).describe('Action to perform: save, load, list (brief), view (detailed), or delete a configuration'),
  name: z.string().optional().describe('Name of the configuration (required for save, load, delete)'),
  detailed: z.boolean().optional().default(false).describe('Show detailed configuration info when listing (only used with list action)'),
  config: z.object({
    description: z.string().optional().describe('Description of what this config does'),
    captureFilter: z.string().optional().describe('BPF capture filter for packet capture'),
    displayFilter: z.string().optional().describe('Wireshark display filter for analysis'),
    outputFormat: z.enum(['json', 'fields', 'text']).optional().describe('Output format for analysis'),
    customFields: z.string().optional().describe('Custom field list for fields format'),
    timeout: z.number().optional().describe('Timeout in seconds for capture sessions'),
    maxPackets: z.number().optional().describe('Maximum packets to capture'),
    interface: z.string().optional().describe('Network interface to use')
  }).optional().describe('Configuration object (required for save action)')
};

/**
 * Tool handler for managing filter configurations
 * Allows LLMs to save, load, list, and delete reusable filter configurations
 */
export async function manageConfigHandler(args: any) {
  try {
    const { action, name, config, detailed } = args;

    switch (action) {
      case 'save':
        if (!name || !config) {
          return {
            content: [{
              type: 'text' as const,
              text: 'Error: Both name and config are required for save action.',
            }],
            isError: true
          };
        }

        const filterConfig: FilterConfig = {
          name,
          ...config
        };

        await saveFilterConfig(filterConfig);
        return {
          content: [{
            type: 'text' as const,
            text: `Configuration '${name}' saved successfully!\n\nSaved config:\n${JSON.stringify(filterConfig, null, 2)}`,
          }],
        };

      case 'load':
        if (!name) {
          return {
            content: [{
              type: 'text' as const,
              text: 'Error: Name is required for load action.',
            }],
            isError: true
          };
        }

        const loadedConfig = await loadFilterConfig(name);
        if (!loadedConfig) {
          return {
            content: [{
              type: 'text' as const,
              text: `Error: Configuration '${name}' not found.`,
            }],
            isError: true
          };
        }

        return {
          content: [{
            type: 'text' as const,
            text: `Configuration '${name}' loaded:\n\n${JSON.stringify(loadedConfig, null, 2)}`,
          }],
        };

      case 'list':
        const allConfigs = await listFilterConfigs();
        if (allConfigs.length === 0) {
          return {
            content: [{
              type: 'text' as const,
              text: 'No saved configurations found.',
            }],
          };
        }

        if (detailed) {
          // Show detailed information for all configurations
          const detailedList = allConfigs.map(cfg => {
            const configDetails = [
              `Name: ${cfg.name}`,
              ...(cfg.description ? [`Description: ${cfg.description}`] : []),
              ...(cfg.captureFilter ? [`Capture Filter: ${cfg.captureFilter}`] : []),
              ...(cfg.displayFilter ? [`Display Filter: ${cfg.displayFilter}`] : []),
              ...(cfg.outputFormat ? [`Output Format: ${cfg.outputFormat}`] : []),
              ...(cfg.customFields ? [`Custom Fields: ${cfg.customFields}`] : []),
              ...(cfg.interface ? [`Interface: ${cfg.interface}`] : []),
              ...(cfg.timeout ? [`Timeout: ${cfg.timeout}s`] : []),
              ...(cfg.maxPackets ? [`Max Packets: ${cfg.maxPackets}`] : [])
            ];
            return configDetails.join('\n  ');
          }).join('\n\n' + '─'.repeat(50) + '\n\n');

          return {
            content: [{
              type: 'text' as const,
              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.`,
            }],
          };
        } else {
          // Show brief list (existing behavior)
          const configList = allConfigs.map(cfg => 
            `• ${cfg.name}${cfg.description ? `: ${cfg.description}` : ''}`
          ).join('\n');

          return {
            content: [{
              type: 'text' as const,
              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.`,
            }],
          };
        }

      case 'view':
        const allConfigsForView = await listFilterConfigs();
        if (allConfigsForView.length === 0) {
          return {
            content: [{
              type: 'text' as const,
              text: 'No saved configurations found.',
            }],
          };
        }

        const configDetails = allConfigsForView.map(cfg => 
          `${cfg.name}:\n${JSON.stringify(cfg, null, 2)}`
        ).join('\n\n' + '─'.repeat(60) + '\n\n');

        return {
          content: [{
            type: 'text' as const,
            text: `All configurations (${allConfigsForView.length}) - Full Details:\n\n${'─'.repeat(60)}\n\n${configDetails}\n\n${'─'.repeat(60)}`,
          }],
        };

      case 'delete':
        if (!name) {
          return {
            content: [{
              type: 'text' as const,
              text: 'Error: Name is required for delete action.',
            }],
            isError: true
          };
        }

        const deleted = await deleteFilterConfig(name);
        if (!deleted) {
          return {
            content: [{
              type: 'text' as const,
              text: `Error: Configuration '${name}' not found.`,
            }],
            isError: true
          };
        }

        return {
          content: [{
            type: 'text' as const,
            text: `Configuration '${name}' deleted successfully.`,
          }],
        };

      default:
        return {
          content: [{
            type: 'text' as const,
            text: `Error: Unknown action '${action}'. Use save, load, list, view, or delete.`,
          }],
          isError: true
        };
    }
  } catch (error: any) {
    console.error(`Error managing config: ${error.message}`);
    return { 
      content: [{ type: 'text' as const, text: `Error: ${error.message}` }], 
      isError: true 
    };
  }
} 
```

--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
import { promisify } from "util";
import { exec } from "child_process";
import which from "which";
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import { OutputFormat, TsharkEnvironment, FilterConfig, ConfigFile } from "./types.js";

// Promisify exec for async/await usage
const execAsync = promisify(exec);

// Get the directory of this file and construct config path relative to project root
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CONFIG_FILE_PATH = path.join(__dirname, '..', 'sharkmcp-configs.json');

/**
 * Dynamically locate tshark executable with cross-platform support
 */
export async function findTshark(): Promise<string> {
  // First, try to find tshark in PATH
  try {
    const tsharkPath = await which('tshark');
    if (!tsharkPath) {
      throw new Error('tshark not found in PATH');
    }
    const pathString = Array.isArray(tsharkPath) ? tsharkPath[0] : tsharkPath;
    
    // Verify the executable works
    await execAsync(`"${pathString}" -v`, { timeout: 5000 });
    console.error(`Found tshark at: ${pathString}`);
    return pathString;
  } catch (err: any) {
    console.error('tshark not found in PATH, trying fallback locations...');
  }

  // Platform-specific fallback paths
  const getFallbackPaths = (): string[] => {
    switch (process.platform) {
      case 'win32':
        return [
          'C:\\Program Files\\Wireshark\\tshark.exe',
          'C:\\Program Files (x86)\\Wireshark\\tshark.exe',
          ...(process.env.ProgramFiles ? [`${process.env.ProgramFiles}\\Wireshark\\tshark.exe`] : []),
          ...(process.env['ProgramFiles(x86)'] ? [`${process.env['ProgramFiles(x86)']}\\Wireshark\\tshark.exe`] : [])
        ];
      
      case 'darwin':
        return [
          '/opt/homebrew/bin/tshark',
          '/usr/local/bin/tshark',
          '/Applications/Wireshark.app/Contents/MacOS/tshark',
          '/usr/bin/tshark'
        ];
      
      case 'linux':
        return [
          '/usr/bin/tshark',
          '/usr/local/bin/tshark',
          '/snap/bin/tshark',
          '/usr/sbin/tshark'
        ];
      
      default:
        return ['/usr/bin/tshark', '/usr/local/bin/tshark'];
    }
  };

  // Try fallback paths
  const fallbackPaths = getFallbackPaths();
  for (const candidatePath of fallbackPaths) {
    try {
      await execAsync(`"${candidatePath}" -v`, { timeout: 5000 });
      console.error(`Found tshark at fallback: ${candidatePath}`);
      return candidatePath;
    } catch {
      // Continue to next fallback
    }
  }

  throw new Error(
    'tshark not found. Please install Wireshark (https://www.wireshark.org/download.html) and ensure tshark is in your PATH.'
  );
}

/**
 * Load config file, creating default if it doesn't exist
 */
export async function loadConfigFile(): Promise<ConfigFile> {
  try {
    const configContent = await fs.readFile(CONFIG_FILE_PATH, 'utf8');
    return JSON.parse(configContent);
  } catch (error) {
    // Create default config file if it doesn't exist
    const defaultConfig: ConfigFile = {
      version: "0.1.0",
      configs: {}
    };
    await saveConfigFile(defaultConfig);
    return defaultConfig;
  }
}

/**
 * Save config file
 */
export async function saveConfigFile(config: ConfigFile): Promise<void> {
  await fs.writeFile(CONFIG_FILE_PATH, JSON.stringify(config, null, 2));
}

/**
 * Save a filter configuration
 */
export async function saveFilterConfig(filterConfig: FilterConfig): Promise<void> {
  const configFile = await loadConfigFile();
  configFile.configs[filterConfig.name] = filterConfig;
  await saveConfigFile(configFile);
}

/**
 * Load a filter configuration by name
 */
export async function loadFilterConfig(name: string): Promise<FilterConfig | null> {
  const configFile = await loadConfigFile();
  return configFile.configs[name] || null;
}

/**
 * List all available filter configurations
 */
export async function listFilterConfigs(): Promise<FilterConfig[]> {
  const configFile = await loadConfigFile();
  return Object.values(configFile.configs);
}

/**
 * Delete a filter configuration
 */
export async function deleteFilterConfig(name: string): Promise<boolean> {
  const configFile = await loadConfigFile();
  if (configFile.configs[name]) {
    delete configFile.configs[name];
    await saveConfigFile(configFile);
    return true;
  }
  return false;
}

/**
 * Process tshark output based on format
 */
export function processTsharkOutput(
  stdout: string,
  outputFormat: OutputFormat
): string {
  switch (outputFormat) {
    case 'json':
      // Try to parse and format JSON for readability
      try {
        const parsed = JSON.parse(stdout);
        return JSON.stringify(parsed, null, 2);
      } catch {
        return stdout; // Return raw if parsing fails
      }
    case 'fields':
    case 'text':
    default:
      return stdout; // Return raw output
  }
}

/**
 * Reusable function for PCAP analysis with comprehensive cross-platform error handling
 */
export async function analyzePcap(
  filePath: string,
  displayFilter: string = '',
  outputFormat: OutputFormat = 'text',
  customFields?: string,
  sslKeylogFile?: string
): Promise<string> {
  const tsharkPath = await findTshark();
      
  // Set up SSL keylog for decryption during analysis
  const analysisEnv: TsharkEnvironment = Object.fromEntries(
    Object.entries(process.env).filter(([_, value]) => value !== undefined)
  ) as TsharkEnvironment;
  
  const keylogToUse = sslKeylogFile || process.env.SSLKEYLOGFILE;
  if (keylogToUse) {
    console.error(`Using SSL keylog file for decryption: ${keylogToUse}`);
    analysisEnv.SSLKEYLOGFILE = keylogToUse;
  }
  
  // Build command based on output format using absolute tshark path
  let command: string;
  const sslOptions = keylogToUse ? `-o tls.keylog_file:"${keylogToUse}"` : '';
  const filterOption = displayFilter ? `-Y "${displayFilter}"` : '';
  const quotedTsharkPath = `"${tsharkPath}"`;
  
  switch (outputFormat) {
    case 'json':
      command = `${quotedTsharkPath} -r "${filePath}" ${sslOptions} ${filterOption} -T json`;
      break;
    case 'fields':
      const fieldsToUse = customFields || 'frame.number,frame.time_relative,ip.src,ip.dst,tcp.srcport,tcp.dstport';
      const fieldArgs = fieldsToUse.split(',').map(field => `-e ${field.trim()}`).join(' ');
      command = `${quotedTsharkPath} -r "${filePath}" ${sslOptions} ${filterOption} -T fields ${fieldArgs}`;
      break;
    case 'text':
    default:
      command = `${quotedTsharkPath} -r "${filePath}" ${sslOptions} ${filterOption}`;
      break;
  }
  
  // Execution options with increased buffer
  const execOptions = { 
    env: analysisEnv,
    maxBuffer: 50 * 1024 * 1024 // 50MB buffer
  };
  
  console.error(`Analyzing capture with command: ${command}`);
  const { stdout } = await execAsync(command, execOptions);
  return processTsharkOutput(stdout, outputFormat);
}

/**
 * Trim output if it exceeds maximum character limits
 * Different formats have different optimal limits for readability
 */
export function trimOutput(output: string, outputFormat: OutputFormat): string {
  // Format-specific limits for optimal readability
  const maxChars = outputFormat === 'json' ? 500000 : 
                   outputFormat === 'fields' ? 800000 : 
                   720000; // text format default
  
  if (output.length > maxChars) {
    const trimPoint = maxChars - 500;
    const formatInfo = outputFormat !== 'text' ? ` (${outputFormat} format)` : '';
    const trimmed = output.substring(0, trimPoint) + `\n\n... [Output truncated due to size${formatInfo}] ...`;
    console.error(`Trimmed ${outputFormat} output from ${output.length} to ${maxChars} chars`);
    return trimmed;
  }
  return output;
} 
```

--------------------------------------------------------------------------------
/test/integration.test.js:
--------------------------------------------------------------------------------

```javascript
/**
 * Integration Tests for SharkMCP Server
 * Tests the full MCP server functionality using the SDK client
 */

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn } from "child_process";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import process from "process";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectRoot = join(__dirname, '..');

/**
 * Test suite configuration
 */
const TEST_CONFIG = {
  serverPath: join(projectRoot, 'dist', 'index.js'),
  testInterface: process.platform === 'darwin' ? 'en0' : 'eth0', // Adjust based on platform
  captureTimeout: 12, // Slightly longer than config timeout to ensure completion
  configName: 'integration_test'
};

/**
 * Utility function to wait for a specified time
 */
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Generate some network traffic to ensure we capture packets
 */
async function generateNetworkTraffic() {
  console.log('Generating network traffic...');
  
  // Create multiple concurrent network requests to generate traffic
  const trafficPromises = [
    // HTTP requests
    fetch('http://httpbin.org/get').catch(() => {}),
    fetch('http://example.com').catch(() => {}),
    // DNS lookups via fetch will generate UDP traffic
    fetch('http://google.com').catch(() => {}),
    fetch('http://github.com').catch(() => {}),
  ];
  
  // Don't wait for all to complete, just start them
  await Promise.allSettled(trafficPromises.slice(0, 2)); // Wait for first 2
  console.log('Network traffic generated');
}

/**
 * Extract packet count from tshark JSON output
 */
function countPacketsFromOutput(output, outputFormat) {
  if (!output || output.trim() === '') {
    return 0;
  }
  
  try {
    switch (outputFormat) {
      case 'json':
        // For JSON format, parse and count array elements
        const parsed = JSON.parse(output);
        if (Array.isArray(parsed)) {
          return parsed.length;
        } else if (parsed._source) {
          // Single packet format
          return 1;
        }
        return 0;
        
      case 'fields':
        // For fields format, count non-empty lines
        return output.split('\n').filter(line => line.trim().length > 0).length;
        
      case 'text':
      default:
        // For text format, count lines that look like packet headers
        const lines = output.split('\n');
        return lines.filter(line => 
          line.match(/^\s*\d+\s+\d+\.\d+/) || // Standard packet line
          line.includes('Ethernet') || 
          line.includes('Internet Protocol')
        ).length;
    }
  } catch (error) {
    console.warn(`Warning: Failed to parse output for packet counting: ${error.message}`);
    // Fallback: count non-empty lines
    return output.split('\n').filter(line => line.trim().length > 0).length;
  }
}

/**
 * Main integration test runner
 */
async function runIntegrationTests() {
  console.log('Starting SharkMCP Integration Tests');
  console.log(`Project root: ${projectRoot}`);
  console.log(`Server path: ${TEST_CONFIG.serverPath}`);
  console.log(`Test interface: ${TEST_CONFIG.testInterface}`);
  
  let client;
  let transport;
  
  try {
    // Initialize MCP client with server transport
    console.log('\nSetting up MCP client transport...');
    transport = new StdioClientTransport({
      command: "node",
      args: [TEST_CONFIG.serverPath]
    });

    client = new Client({
      name: "sharkmcp-integration-test",
      version: "1.0.0"
    });

    console.log('Connecting to MCP server...');
    await client.connect(transport);
    console.log('Successfully connected to MCP server');

    // Test 1: List available tools
    console.log('\nTest 1: Listing available tools...');
    const tools = await client.listTools();
    console.log(`Found ${tools.tools.length} tools:`);
    tools.tools.forEach(tool => {
      console.log(`  - ${tool.name}: ${tool.description}`);
    });
    
    const expectedTools = ['start_capture_session', 'stop_capture_session', 'analyze_pcap_file', 'manage_config'];
    const foundTools = tools.tools.map(t => t.name);
    const missingTools = expectedTools.filter(tool => !foundTools.includes(tool));
    
    if (missingTools.length > 0) {
      throw new Error(`Missing expected tools: ${missingTools.join(', ')}`);
    }
    console.log('All expected tools found');

    // Test 2: Load and verify test configuration
    console.log('\nTest 2: Loading test configuration...');
    const configResult = await client.callTool({
      name: "manage_config",
      arguments: {
        action: "load",
        name: TEST_CONFIG.configName
      }
    });
    
    if (configResult.isError) {
      throw new Error(`Failed to load test config: ${configResult.content[0].text}`);
    }
    console.log('Test configuration loaded successfully');
    console.log(configResult.content[0].text);

    // Test 3: Start capture session using saved config
    console.log('\nTest 3: Starting packet capture session...');
    const startResult = await client.callTool({
      name: "start_capture_session",
      arguments: {
        configName: TEST_CONFIG.configName,
        interface: TEST_CONFIG.testInterface
      }
    });
    
    if (startResult.isError) {
      throw new Error(`Failed to start capture: ${startResult.content[0].text}`);
    }
    
    const startText = startResult.content[0].text;
    console.log('Capture session started');
    console.log(startText);
    
    // Extract session ID from response
    const sessionIdMatch = startText.match(/Session ID: ([\w_]+)/);
    if (!sessionIdMatch) {
      throw new Error('Could not extract session ID from start response');
    }
    const sessionId = sessionIdMatch[1];
    console.log(`Session ID: ${sessionId}`);

    // Test 4: Generate network traffic during capture
    console.log('\nTest 4: Generating network traffic...');
    await sleep(2000); // Wait 2 seconds after starting capture
    await generateNetworkTraffic();
    
    // Wait for remaining capture time
    const remainingTime = (TEST_CONFIG.captureTimeout - 3) * 1000; // 3 seconds already passed
    console.log(`Waiting ${remainingTime/1000}s for capture to complete...`);
    await sleep(remainingTime);

    // Test 5: Stop capture and analyze results
    console.log('\nTest 5: Stopping capture and analyzing results...');
    const stopResult = await client.callTool({
      name: "stop_capture_session",
      arguments: {
        sessionId: sessionId,
        outputFormat: "json"
      }
    });
    
    if (stopResult.isError) {
      throw new Error(`Failed to stop capture: ${stopResult.content[0].text}`);
    }
    
    const stopText = stopResult.content[0].text;
    console.log('Capture session stopped and analyzed');
    
    // Test 6: Extract and count packets
    console.log('\nTest 6: Counting captured packets...');
    
    // Extract the JSON results section
    const resultsMatch = stopText.match(/Packet Analysis Results:\n(.*)/s);
    if (!resultsMatch) {
      console.warn('Could not extract packet analysis results from response');
      console.log('Full response:');
      console.log(stopText);
    } else {
      const packetData = resultsMatch[1];
      const packetCount = countPacketsFromOutput(packetData, 'json');
      
      console.log(`Packet count: ${packetCount}`);
      
      if (packetCount === 0) {
        console.warn('No packets captured - this could indicate:');
        console.warn('   - No network traffic on interface during capture');
        console.warn('   - Interface name incorrect for this system');
        console.warn('   - Permission issues with packet capture');
        console.warn('   - tshark not working properly');
      } else {
        console.log(`Successfully captured ${packetCount} packets`);
      }
      
      // Show some sample output
      if (packetData.length > 0) {
        const sampleLength = Math.min(500, packetData.length);
        console.log('\nSample output (first 500 chars):');
        console.log(packetData.substring(0, sampleLength));
        if (packetData.length > sampleLength) {
          console.log('... (truncated)');
        }
      }
    }

    // Test 7: Test PCAP file analysis (if we have the test file)
    console.log('\nTest 7: Testing PCAP file analysis...');
    try {
      const pcapResult = await client.callTool({
        name: "analyze_pcap_file",
        arguments: {
          filePath: join(projectRoot, 'test', 'dump.pcapng'),
          outputFormat: "json",
          displayFilter: ""
        }
      });
      
      if (!pcapResult.isError) {
        const pcapText = pcapResult.content[0].text;
        const pcapResultsMatch = pcapText.match(/Packet Analysis Results:\n(.*)/s);
        
        if (pcapResultsMatch) {
          const pcapPacketData = pcapResultsMatch[1];
          const pcapPacketCount = countPacketsFromOutput(pcapPacketData, 'json');
          console.log(`PCAP file analysis successful: ${pcapPacketCount} packets found`);
        } else {
          console.log('PCAP file analysis completed (format parsing issue)');
        }
      } else {
        console.log('PCAP file analysis failed (test file may not exist)');
      }
    } catch (error) {
      console.log(`PCAP file analysis test skipped: ${error.message}`);
    }

    // Test 8: Test TLS handshake filtering on dump.pcapng
    console.log('\nTest 8: Testing TLS handshake filter on dump.pcapng...');
    try {
      const tlsResult = await client.callTool({
        name: "analyze_pcap_file",
        arguments: {
          filePath: join(projectRoot, 'test', 'dump.pcapng'),
          outputFormat: "json",
          displayFilter: "tls.handshake.type == 1"
        }
      });
      
      if (!tlsResult.isError) {
        const tlsText = tlsResult.content[0].text;
        const tlsResultsMatch = tlsText.match(/Packet Analysis Results:\n(.*)/s);
        
        if (tlsResultsMatch) {
          const tlsPacketData = tlsResultsMatch[1];
          const tlsPacketCount = countPacketsFromOutput(tlsPacketData, 'json');
          
          if (tlsPacketCount > 0) {
            console.log(`TLS handshake filter successful: Found ${tlsPacketCount} TLS Client Hello packets`);
            
            // Show a sample of the TLS handshake data
            if (tlsPacketData.length > 0) {
              const sampleLength = Math.min(300, tlsPacketData.length);
              console.log('\nSample TLS handshake data (first 300 chars):');
              console.log(tlsPacketData.substring(0, sampleLength));
              if (tlsPacketData.length > sampleLength) {
                console.log('... (truncated)');
              }
            }
          } else {
            console.log('TLS handshake filter returned no packets - dump.pcapng may not contain TLS Client Hello packets');
          }
        } else {
          console.log('TLS handshake filter completed but could not parse results');
        }
      } else {
        console.log(`TLS handshake filter failed: ${tlsResult.content[0].text}`);
      }
    } catch (error) {
      console.log(`TLS handshake filter test failed: ${error.message}`);
    }

    console.log('\nIntegration tests completed successfully!');
    console.log('\nTest Summary:');
    console.log('- MCP server connection and communication');
    console.log('- Tool discovery and listing');
    console.log('- Configuration management');
    console.log('- Packet capture session lifecycle');
    console.log('- Network traffic generation and capture');
    console.log('- Packet analysis and counting');
    console.log('- PCAP file analysis with display filters');
    console.log('- TLS handshake packet filtering');
    console.log('- Error handling and edge cases');
    
    return true;
    
  } catch (error) {
    console.error('\nIntegration test failed:');
    console.error(error.message);
    console.error('\nStack trace:');
    console.error(error.stack);
    return false;
    
  } finally {
    // Clean up
    if (client && transport) {
      try {
        console.log('\nCleaning up MCP connection...');
        await client.close();
        console.log('MCP connection closed');
      } catch (error) {
        console.warn(`Warning during cleanup: ${error.message}`);
      }
    }
  }
}

// Run tests if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
  const success = await runIntegrationTests();
  process.exit(success ? 0 : 1);
}

export { runIntegrationTests }; 
```