# 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 };
```