mcpcap 0.4.6__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mcpcap/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.4.6'
32
- __version_tuple__ = version_tuple = (0, 4, 6)
31
+ __version__ = version = '0.5.0'
32
+ __version_tuple__ = version_tuple = (0, 5, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
mcpcap/cli.py CHANGED
@@ -20,27 +20,16 @@ def main():
20
20
  int: Exit code (0 for success, 1 for error)
21
21
 
22
22
  Raises:
23
- ValueError: If the provided PCAP path is invalid
24
23
  KeyboardInterrupt: If the user interrupts the server
25
24
  Exception: For any unexpected errors during server operation
26
25
  """
27
26
  parser = argparse.ArgumentParser(description="mcpcap MCP Server")
28
27
 
29
- # PCAP source options (mutually exclusive)
30
- source_group = parser.add_mutually_exclusive_group(required=True)
31
- source_group.add_argument(
32
- "--pcap-path", help="Path to PCAP file or directory containing PCAP files"
33
- )
34
- source_group.add_argument(
35
- "--pcap-url",
36
- help="HTTP URL to PCAP file (direct link) or directory containing PCAP files",
37
- )
38
-
39
28
  # Analysis options
40
29
  parser.add_argument(
41
30
  "--modules",
42
- help="Comma-separated list of modules to load (default: dns)",
43
- default="dns",
31
+ help="Comma-separated list of modules to load (default: dns,dhcp,icmp)",
32
+ default="dns,dhcp,icmp",
44
33
  )
45
34
  parser.add_argument(
46
35
  "--max-packets",
@@ -52,13 +41,11 @@ def main():
52
41
 
53
42
  try:
54
43
  # Parse modules and automatically set protocols to match
55
- modules = args.modules.split(",") if args.modules else ["dns"]
44
+ modules = args.modules.split(",") if args.modules else ["dns", "dhcp", "icmp"]
56
45
  protocols = modules # Protocols automatically match loaded modules
57
46
 
58
47
  # Initialize configuration
59
48
  config = Config(
60
- pcap_path=args.pcap_path,
61
- pcap_url=args.pcap_url,
62
49
  modules=modules,
63
50
  protocols=protocols,
64
51
  max_packets=args.max_packets,
mcpcap/core/config.py CHANGED
@@ -1,18 +1,11 @@
1
1
  """Configuration management for mcpcap."""
2
2
 
3
- import os
4
- from urllib.parse import urljoin, urlparse
5
-
6
- import requests
7
-
8
3
 
9
4
  class Config:
10
5
  """Configuration management for mcpcap server."""
11
6
 
12
7
  def __init__(
13
8
  self,
14
- pcap_path: str | None = None,
15
- pcap_url: str | None = None,
16
9
  modules: list[str] | None = None,
17
10
  protocols: list[str] | None = None,
18
11
  max_packets: int | None = None,
@@ -20,211 +13,17 @@ class Config:
20
13
  """Initialize configuration.
21
14
 
22
15
  Args:
23
- pcap_path: Path to directory containing PCAP files
24
- pcap_url: HTTP server URL containing PCAP files
25
16
  modules: List of modules to load
26
17
  protocols: List of protocols to analyze
27
18
  max_packets: Maximum number of packets to analyze per file
28
19
  """
29
- self.pcap_path = pcap_path
30
- self.pcap_url = pcap_url
31
- self.modules = modules or ["dns"]
32
- self.protocols = protocols or ["dns"]
20
+ self.modules = modules or ["dns", "dhcp"]
21
+ self.protocols = protocols or ["dns", "dhcp"]
33
22
  self.max_packets = max_packets
34
- self.is_remote = pcap_url is not None
35
- self.is_direct_file_url = False # Will be set during validation
36
- self.is_direct_file_path = (
37
- False # Will be set during validation for local files
38
- )
39
23
 
40
24
  self._validate_configuration()
41
25
 
42
26
  def _validate_configuration(self) -> None:
43
27
  """Validate the configuration parameters."""
44
- if not self.pcap_path and not self.pcap_url:
45
- raise ValueError("Either --pcap-path or --pcap-url must be specified")
46
-
47
- if self.pcap_path and self.pcap_url:
48
- raise ValueError("Cannot specify both --pcap-path and --pcap-url")
49
-
50
- if self.pcap_path:
51
- self._validate_pcap_path()
52
-
53
- if self.pcap_url:
54
- self._validate_pcap_url()
55
-
56
28
  if self.max_packets is not None and self.max_packets <= 0:
57
29
  raise ValueError("max_packets must be a positive integer")
58
-
59
- def _validate_pcap_path(self) -> None:
60
- """Validate that the PCAP path exists and is either a directory or a PCAP file."""
61
- if not os.path.exists(self.pcap_path):
62
- raise ValueError(f"PCAP path '{self.pcap_path}' does not exist")
63
-
64
- if os.path.isfile(self.pcap_path):
65
- # Check if it's a PCAP file
66
- if not self.pcap_path.lower().endswith((".pcap", ".pcapng", ".cap")):
67
- raise ValueError(
68
- f"File '{self.pcap_path}' is not a supported PCAP file (.pcap/.pcapng/.cap)"
69
- )
70
- self.is_direct_file_path = True
71
- elif os.path.isdir(self.pcap_path):
72
- self.is_direct_file_path = False
73
- else:
74
- raise ValueError(f"'{self.pcap_path}' is neither a file nor a directory")
75
-
76
- def _validate_pcap_url(self) -> None:
77
- """Validate that the PCAP URL is accessible."""
78
- try:
79
- parsed = urlparse(self.pcap_url)
80
- if not parsed.scheme or not parsed.netloc:
81
- raise ValueError(f"Invalid URL format: {self.pcap_url}")
82
-
83
- # Determine if this is a direct file URL or directory URL
84
- self.is_direct_file_url = self._is_direct_file_url()
85
-
86
- # Test connectivity with a HEAD request
87
- response = requests.head(self.pcap_url, timeout=10)
88
- if response.status_code >= 400:
89
- raise ValueError(
90
- f"Cannot access PCAP URL: {self.pcap_url} (HTTP {response.status_code})"
91
- )
92
-
93
- except requests.RequestException as e:
94
- raise ValueError(
95
- f"Cannot connect to PCAP URL '{self.pcap_url}': {str(e)}"
96
- ) from e
97
-
98
- def _is_direct_file_url(self) -> bool:
99
- """Determine if the URL points directly to a PCAP file."""
100
- parsed = urlparse(self.pcap_url)
101
- path = parsed.path.lower()
102
-
103
- # Check if URL ends with a PCAP file extension
104
- return (
105
- path.endswith(".pcap") or path.endswith(".pcapng") or path.endswith(".cap")
106
- )
107
-
108
- def get_pcap_file_path(self, pcap_file: str) -> str:
109
- """Get full path or URL to a PCAP file.
110
-
111
- Args:
112
- pcap_file: Filename or relative path to PCAP file
113
-
114
- Returns:
115
- Full path or URL to the PCAP file
116
- """
117
- if self.is_remote:
118
- # If it's already a full URL, return as-is
119
- if pcap_file.startswith("http"):
120
- return pcap_file
121
-
122
- # If this is a direct file URL, return the URL directly
123
- if self.is_direct_file_url:
124
- return self.pcap_url
125
-
126
- # Otherwise, treat as directory and join with filename
127
- return urljoin(self.pcap_url.rstrip("/") + "/", pcap_file)
128
- else:
129
- # Local file handling
130
- if os.path.isabs(pcap_file):
131
- return pcap_file
132
-
133
- # If this is a direct file path, return it directly
134
- if self.is_direct_file_path:
135
- return self.pcap_path
136
-
137
- # Otherwise, join with directory
138
- return os.path.join(self.pcap_path, pcap_file)
139
-
140
- def list_pcap_files(self) -> list[str]:
141
- """List all PCAP files in the configured directory or remote URL.
142
-
143
- Returns:
144
- List of PCAP filenames
145
- """
146
- if self.is_remote:
147
- return self._list_remote_pcap_files()
148
- else:
149
- if self.is_direct_file_path:
150
- # Return just the filename from the direct file path
151
- return [os.path.basename(self.pcap_path)]
152
- else:
153
- # List files in directory
154
- try:
155
- return [
156
- f
157
- for f in os.listdir(self.pcap_path)
158
- if f.endswith((".pcap", ".pcapng", ".cap"))
159
- ]
160
- except Exception:
161
- return []
162
-
163
- def _list_remote_pcap_files(self) -> list[str]:
164
- """List PCAP files from a remote HTTP server.
165
-
166
- Returns:
167
- List of PCAP filenames found on the remote server
168
- """
169
- # If this is a direct file URL, return just that filename
170
- if self.is_direct_file_url:
171
- filename = os.path.basename(urlparse(self.pcap_url).path)
172
- return [filename] if filename else []
173
-
174
- # Otherwise try to parse directory listing
175
- try:
176
- response = requests.get(self.pcap_url, timeout=30)
177
- response.raise_for_status()
178
-
179
- # Parse HTML to find .pcap and .pcapng files
180
- # This is a simple implementation that looks for href attributes
181
- import re
182
-
183
- pcap_files = []
184
-
185
- # Look for links to .pcap, .pcapng, and .cap files
186
- pattern = r'href=["\']([^"\']*\.(?:pcap|pcapng|cap))["\']'
187
- matches = re.findall(pattern, response.text, re.IGNORECASE)
188
-
189
- for match in matches:
190
- # Extract just the filename, not the full path
191
- filename = os.path.basename(match)
192
- if filename and filename not in pcap_files:
193
- pcap_files.append(filename)
194
-
195
- return sorted(pcap_files)
196
-
197
- except requests.RequestException:
198
- return []
199
-
200
- def download_pcap_file(self, pcap_file: str, local_path: str) -> str:
201
- """Download a remote PCAP file to local storage.
202
-
203
- Args:
204
- pcap_file: Name of the PCAP file to download
205
- local_path: Local path to save the file
206
-
207
- Returns:
208
- Local path to the downloaded file
209
- """
210
- if not self.is_remote:
211
- raise ValueError("Cannot download file: not using remote source")
212
-
213
- url = self.get_pcap_file_path(pcap_file)
214
-
215
- try:
216
- response = requests.get(url, timeout=60, stream=True)
217
- response.raise_for_status()
218
-
219
- os.makedirs(os.path.dirname(local_path), exist_ok=True)
220
-
221
- with open(local_path, "wb") as f:
222
- for chunk in response.iter_content(chunk_size=8192):
223
- f.write(chunk)
224
-
225
- return local_path
226
-
227
- except requests.RequestException as e:
228
- raise ValueError(
229
- f"Failed to download PCAP file '{pcap_file}': {str(e)}"
230
- ) from e
mcpcap/core/server.py CHANGED
@@ -4,6 +4,7 @@ from fastmcp import FastMCP
4
4
 
5
5
  from ..modules.dhcp import DHCPModule
6
6
  from ..modules.dns import DNSModule
7
+ from ..modules.icmp import ICMPModule
7
8
  from .config import Config
8
9
 
9
10
 
@@ -25,6 +26,8 @@ class MCPServer:
25
26
  self.modules["dns"] = DNSModule(config)
26
27
  if "dhcp" in self.config.modules:
27
28
  self.modules["dhcp"] = DHCPModule(config)
29
+ if "icmp" in self.config.modules:
30
+ self.modules["icmp"] = ICMPModule(config)
28
31
 
29
32
  # Register tools
30
33
  self._register_tools()
@@ -38,25 +41,21 @@ class MCPServer:
38
41
  # Register tools for each loaded module
39
42
  for module_name, module in self.modules.items():
40
43
  if module_name == "dns":
41
- self.mcp.tool(module.list_dns_packets)
44
+ self.mcp.tool(module.analyze_dns_packets)
42
45
  elif module_name == "dhcp":
43
- self.mcp.tool(module.list_dhcp_packets)
44
-
45
- # Register shared list_pcap_files tool (same for all modules)
46
- if self.modules:
47
- # Use the first available module for listing PCAP files
48
- first_module = next(iter(self.modules.values()))
49
- self.mcp.tool(first_module.list_pcap_files)
46
+ self.mcp.tool(module.analyze_dhcp_packets)
47
+ elif module_name == "icmp":
48
+ self.mcp.tool(module.analyze_icmp_packets)
50
49
 
51
50
  def run(self) -> None:
52
51
  """Start the MCP server."""
53
52
  import sys
54
53
 
55
54
  # Log to stderr to avoid breaking MCP JSON-RPC protocol
56
- source = (
57
- self.config.pcap_url if self.config.is_remote else self.config.pcap_path
55
+ enabled_modules = ", ".join(self.config.modules)
56
+ print(
57
+ f"Starting mcpcap MCP server with modules: {enabled_modules}",
58
+ file=sys.stderr,
58
59
  )
59
- source_type = "remote URL" if self.config.is_remote else "directory"
60
- print(f"Starting MCP server with PCAP {source_type}: {source}", file=sys.stderr)
61
60
 
62
61
  self.mcp.run()
mcpcap/modules/base.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Base module interface for protocol analyzers."""
2
2
 
3
+ import os
4
+ import tempfile
3
5
  from abc import ABC, abstractmethod
4
6
  from typing import Any
5
7
 
@@ -17,20 +19,115 @@ class BaseModule(ABC):
17
19
  """
18
20
  self.config = config
19
21
 
22
+ @property
20
23
  @abstractmethod
21
- def analyze_packets(self, pcap_file: str) -> dict[str, Any]:
22
- """Analyze packets in a PCAP file.
24
+ def protocol_name(self) -> str:
25
+ """Return the name of the protocol this module analyzes."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def _analyze_protocol_file(self, pcap_file: str) -> dict[str, Any]:
30
+ """Analyze a local PCAP file for this protocol.
31
+
32
+ This method should be implemented by each module to perform
33
+ the actual protocol-specific analysis.
23
34
 
24
35
  Args:
25
- pcap_file: Path to the PCAP file
36
+ pcap_file: Path to local PCAP file
26
37
 
27
38
  Returns:
28
- Analysis results as a dictionary
39
+ Analysis results dictionary
29
40
  """
30
41
  pass
31
42
 
32
- @property
33
- @abstractmethod
34
- def protocol_name(self) -> str:
35
- """Return the name of the protocol this module analyzes."""
36
- pass
43
+ def analyze_packets(self, pcap_file: str) -> dict[str, Any]:
44
+ """Analyze packets from a PCAP file (local or remote).
45
+
46
+ Args:
47
+ pcap_file: Path to local PCAP file or HTTP URL to remote PCAP file
48
+
49
+ Returns:
50
+ A structured dictionary containing packet analysis results
51
+ """
52
+ # Check if this is a remote URL or local file
53
+ if pcap_file.startswith(("http://", "https://")):
54
+ return self._handle_remote_analysis(pcap_file)
55
+ else:
56
+ return self._handle_local_analysis(pcap_file)
57
+
58
+ def _handle_remote_analysis(self, pcap_url: str) -> dict[str, Any]:
59
+ """Handle remote PCAP file analysis."""
60
+ try:
61
+ # Download remote file to temporary location
62
+ with tempfile.NamedTemporaryFile(suffix=".pcap", delete=False) as tmp_file:
63
+ temp_path = tmp_file.name
64
+
65
+ local_path = self._download_pcap_file(pcap_url, temp_path)
66
+ result = self._analyze_protocol_file(local_path)
67
+
68
+ # Clean up temporary file
69
+ try:
70
+ os.unlink(local_path)
71
+ except OSError:
72
+ pass # Ignore cleanup errors
73
+
74
+ return result
75
+
76
+ except Exception as e:
77
+ return {
78
+ "error": f"Failed to download PCAP file '{pcap_url}': {str(e)}",
79
+ "pcap_url": pcap_url,
80
+ }
81
+
82
+ def _handle_local_analysis(self, pcap_file: str) -> dict[str, Any]:
83
+ """Handle local PCAP file analysis."""
84
+ # Validate file exists
85
+ if not os.path.exists(pcap_file):
86
+ return {
87
+ "error": f"PCAP file not found: {pcap_file}",
88
+ "pcap_file": pcap_file,
89
+ }
90
+
91
+ # Validate file extension
92
+ if not pcap_file.lower().endswith((".pcap", ".pcapng", ".cap")):
93
+ return {
94
+ "error": f"File '{pcap_file}' is not a supported PCAP file (.pcap/.pcapng/.cap)",
95
+ "pcap_file": pcap_file,
96
+ }
97
+
98
+ try:
99
+ return self._analyze_protocol_file(pcap_file)
100
+ except Exception as e:
101
+ return {
102
+ "error": f"Failed to analyze PCAP file '{pcap_file}': {str(e)}",
103
+ "pcap_file": pcap_file,
104
+ }
105
+
106
+ def _download_pcap_file(self, pcap_url: str, local_path: str) -> str:
107
+ """Download a remote PCAP file to local storage.
108
+
109
+ Args:
110
+ pcap_url: URL of the PCAP file to download
111
+ local_path: Local path to save the file
112
+
113
+ Returns:
114
+ Local path to the downloaded file
115
+ """
116
+ import requests
117
+
118
+ try:
119
+ response = requests.get(pcap_url, timeout=60, stream=True)
120
+ response.raise_for_status()
121
+
122
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
123
+
124
+ with open(local_path, "wb") as f:
125
+ for chunk in response.iter_content(chunk_size=8192):
126
+ f.write(chunk)
127
+
128
+ return local_path
129
+
130
+ except requests.RequestException as e:
131
+ raise ValueError(
132
+ f"Failed to download PCAP file '{pcap_url}': {str(e)}"
133
+ ) from e
mcpcap/modules/dhcp.py CHANGED
@@ -1,7 +1,5 @@
1
1
  """DHCP analysis module."""
2
2
 
3
- import os
4
- import tempfile
5
3
  from typing import Any
6
4
 
7
5
  from fastmcp import FastMCP
@@ -18,129 +16,20 @@ class DHCPModule(BaseModule):
18
16
  """Return the name of the protocol this module analyzes."""
19
17
  return "DHCP"
20
18
 
21
- def list_dhcp_packets(self, pcap_file: str = "") -> dict[str, Any]:
19
+ def analyze_dhcp_packets(self, pcap_file: str) -> dict[str, Any]:
22
20
  """
23
- Analyze DHCP packets from a PCAP file and return a summary of each packet.
21
+ Analyze DHCP packets from a PCAP file and return comprehensive analysis results.
24
22
 
25
23
  Args:
26
- pcap_file: Path to the PCAP file to analyze. Leave empty for direct URL remotes
27
- or when using the first available file in local directories.
24
+ pcap_file: Path to local PCAP file or HTTP URL to remote PCAP file
28
25
 
29
26
  Returns:
30
27
  A structured dictionary containing DHCP packet analysis results
31
28
  """
32
- # Handle remote files
33
- if self.config.is_remote:
34
- # For direct file URLs, always use the URL file (ignore pcap_file parameter)
35
- if self.config.is_direct_file_url:
36
- available_files = self.config.list_pcap_files()
37
- if not available_files:
38
- return {
39
- "error": "No PCAP file found at the provided URL",
40
- "pcap_url": self.config.pcap_url,
41
- }
42
- pcap_file = available_files[0] # Use the actual filename from URL
43
- elif not pcap_file:
44
- # For directory URLs, if no file specified, use the first available
45
- available_files = self.config.list_pcap_files()
46
- if not available_files:
47
- return {
48
- "error": "No PCAP files found at the provided URL",
49
- "pcap_url": self.config.pcap_url,
50
- "available_files": [],
51
- }
52
- pcap_file = available_files[0]
53
-
54
- # Download remote file to temporary location
55
- try:
56
- with tempfile.NamedTemporaryFile(
57
- suffix=".pcap", delete=False
58
- ) as tmp_file:
59
- temp_path = tmp_file.name
60
-
61
- local_path = self.config.download_pcap_file(pcap_file, temp_path)
62
- result = self.analyze_packets(local_path)
63
-
64
- # Clean up temporary file
65
- try:
66
- os.unlink(local_path)
67
- except OSError:
68
- pass # Ignore cleanup errors
69
-
70
- return result
71
-
72
- except Exception as e:
73
- # List available PCAP files for help
74
- available_files = self.config.list_pcap_files()
75
- return {
76
- "error": f"Failed to download PCAP file '{pcap_file}': {str(e)}",
77
- "pcap_url": self.config.pcap_url,
78
- "available_files": available_files,
79
- }
80
-
81
- else:
82
- # Handle local files
83
- if not pcap_file:
84
- # If no file specified, use the first available file
85
- available_files = self.config.list_pcap_files()
86
- if not available_files:
87
- return {
88
- "error": "No PCAP files found in directory",
89
- "pcap_directory": self.config.pcap_path,
90
- "available_files": [],
91
- }
92
- pcap_file = available_files[0]
29
+ return self.analyze_packets(pcap_file)
93
30
 
94
- full_path = self.config.get_pcap_file_path(pcap_file)
95
-
96
- # Check if local file exists
97
- if not os.path.exists(full_path):
98
- available_files = self.config.list_pcap_files()
99
- return {
100
- "error": f"PCAP file '{pcap_file}' not found",
101
- "file_path": full_path,
102
- "available_files": available_files,
103
- "pcap_directory": self.config.pcap_path,
104
- }
105
-
106
- return self.analyze_packets(full_path)
107
-
108
- def list_pcap_files(self) -> str:
109
- """
110
- List all available PCAP files in the configured directory or remote URL.
111
-
112
- Returns:
113
- A list of available PCAP files that can be analyzed
114
- """
115
- files = self.config.list_pcap_files()
116
- source = (
117
- self.config.pcap_url if self.config.is_remote else self.config.pcap_path
118
- )
119
-
120
- if files:
121
- if self.config.is_remote and self.config.is_direct_file_url:
122
- return f"Direct PCAP file URL: {source}\\n- {files[0]}"
123
- elif not self.config.is_remote and self.config.is_direct_file_path:
124
- return f"Direct PCAP file path: {source}\\n- {files[0]}"
125
- else:
126
- source_type = "remote server" if self.config.is_remote else "directory"
127
- return (
128
- f"Available PCAP files in {source_type} {source}:\\n"
129
- + "\\n".join(f"- {f}" for f in sorted(files))
130
- )
131
- else:
132
- source_type = "remote server" if self.config.is_remote else "directory"
133
- return f"No PCAP files found in {source_type} {source}"
134
-
135
- def analyze_packets(self, pcap_file: str) -> dict[str, Any]:
136
- """Analyze DHCP packets in a PCAP file.
137
-
138
- Args:
139
- pcap_file: Path to the PCAP file to analyze
140
-
141
- Returns:
142
- Dictionary containing DHCP packet analysis results
143
- """
31
+ def _analyze_protocol_file(self, pcap_file: str) -> dict[str, Any]:
32
+ """Perform the actual DHCP packet analysis on a local PCAP file."""
144
33
  try:
145
34
  packets = rdpcap(pcap_file)
146
35
  dhcp_packets = [pkt for pkt in packets if pkt.haslayer(BOOTP)]