mcpcap 0.2.2__py3-none-any.whl → 0.3.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 +2 -2
- mcpcap/cli.py +37 -6
- mcpcap/core/config.py +196 -18
- mcpcap/core/server.py +10 -2
- mcpcap/modules/dns.py +108 -20
- {mcpcap-0.2.2.dist-info → mcpcap-0.3.0.dist-info}/METADATA +99 -8
- mcpcap-0.3.0.dist-info/RECORD +17 -0
- mcpcap-0.2.2.dist-info/RECORD +0 -17
- {mcpcap-0.2.2.dist-info → mcpcap-0.3.0.dist-info}/WHEEL +0 -0
- {mcpcap-0.2.2.dist-info → mcpcap-0.3.0.dist-info}/entry_points.txt +0 -0
- {mcpcap-0.2.2.dist-info → mcpcap-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {mcpcap-0.2.2.dist-info → mcpcap-0.3.0.dist-info}/top_level.txt +0 -0
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.
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
31
|
+
__version__ = version = '0.3.0'
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
mcpcap/cli.py
CHANGED
@@ -5,6 +5,7 @@ and server initialization.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import argparse
|
8
|
+
import sys
|
8
9
|
|
9
10
|
from .core import Config, MCPServer
|
10
11
|
|
@@ -23,16 +24,46 @@ def main():
|
|
23
24
|
KeyboardInterrupt: If the user interrupts the server
|
24
25
|
Exception: For any unexpected errors during server operation
|
25
26
|
"""
|
26
|
-
parser = argparse.ArgumentParser(description="
|
27
|
+
parser = argparse.ArgumentParser(description="mcpcap MCP Server")
|
28
|
+
|
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
|
+
# Analysis options
|
40
|
+
parser.add_argument(
|
41
|
+
"--modules",
|
42
|
+
help="Comma-separated list of modules to load (default: dns)",
|
43
|
+
default="dns",
|
44
|
+
)
|
45
|
+
parser.add_argument(
|
46
|
+
"--protocols",
|
47
|
+
help="Comma-separated list of protocols to analyze (default: dns)",
|
48
|
+
default="dns",
|
49
|
+
)
|
27
50
|
parser.add_argument(
|
28
|
-
"--
|
51
|
+
"--max-packets",
|
52
|
+
type=int,
|
53
|
+
help="Maximum number of packets to analyze per file (default: unlimited)",
|
29
54
|
)
|
30
55
|
|
31
56
|
args = parser.parse_args()
|
32
57
|
|
33
58
|
try:
|
34
59
|
# Initialize configuration
|
35
|
-
config = Config(
|
60
|
+
config = Config(
|
61
|
+
pcap_path=args.pcap_path,
|
62
|
+
pcap_url=args.pcap_url,
|
63
|
+
modules=args.modules.split(",") if args.modules else ["dns"],
|
64
|
+
protocols=args.protocols.split(",") if args.protocols else ["dns"],
|
65
|
+
max_packets=args.max_packets,
|
66
|
+
)
|
36
67
|
|
37
68
|
# Create and start MCP server
|
38
69
|
server = MCPServer(config)
|
@@ -40,13 +71,13 @@ def main():
|
|
40
71
|
return 0
|
41
72
|
|
42
73
|
except ValueError as e:
|
43
|
-
print(f"Error: {e}")
|
74
|
+
print(f"Error: {e}", file=sys.stderr)
|
44
75
|
return 1
|
45
76
|
except KeyboardInterrupt:
|
46
|
-
print("\\nServer stopped by user")
|
77
|
+
print("\\nServer stopped by user", file=sys.stderr)
|
47
78
|
return 0
|
48
79
|
except Exception as e:
|
49
|
-
print(f"Unexpected error: {e}")
|
80
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
50
81
|
return 1
|
51
82
|
|
52
83
|
|
mcpcap/core/config.py
CHANGED
@@ -1,52 +1,230 @@
|
|
1
1
|
"""Configuration management for mcpcap."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
from urllib.parse import urljoin, urlparse
|
5
|
+
|
6
|
+
import requests
|
4
7
|
|
5
8
|
|
6
9
|
class Config:
|
7
10
|
"""Configuration management for mcpcap server."""
|
8
11
|
|
9
|
-
def __init__(
|
12
|
+
def __init__(
|
13
|
+
self,
|
14
|
+
pcap_path: str | None = None,
|
15
|
+
pcap_url: str | None = None,
|
16
|
+
modules: list[str] | None = None,
|
17
|
+
protocols: list[str] | None = None,
|
18
|
+
max_packets: int | None = None,
|
19
|
+
):
|
10
20
|
"""Initialize configuration.
|
11
21
|
|
12
22
|
Args:
|
13
23
|
pcap_path: Path to directory containing PCAP files
|
24
|
+
pcap_url: HTTP server URL containing PCAP files
|
25
|
+
modules: List of modules to load
|
26
|
+
protocols: List of protocols to analyze
|
27
|
+
max_packets: Maximum number of packets to analyze per file
|
14
28
|
"""
|
15
29
|
self.pcap_path = pcap_path
|
16
|
-
self.
|
30
|
+
self.pcap_url = pcap_url
|
31
|
+
self.modules = modules or ["dns"]
|
32
|
+
self.protocols = protocols or ["dns"]
|
33
|
+
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
|
+
|
40
|
+
self._validate_configuration()
|
41
|
+
|
42
|
+
def _validate_configuration(self) -> None:
|
43
|
+
"""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
|
+
if self.max_packets is not None and self.max_packets <= 0:
|
57
|
+
raise ValueError("max_packets must be a positive integer")
|
17
58
|
|
18
59
|
def _validate_pcap_path(self) -> None:
|
19
|
-
"""Validate that the PCAP path exists and is a directory."""
|
60
|
+
"""Validate that the PCAP path exists and is either a directory or a PCAP file."""
|
20
61
|
if not os.path.exists(self.pcap_path):
|
21
|
-
raise ValueError(f"PCAP
|
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()
|
22
85
|
|
23
|
-
|
24
|
-
|
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
|
+
)
|
25
107
|
|
26
108
|
def get_pcap_file_path(self, pcap_file: str) -> str:
|
27
|
-
"""Get full path to a PCAP file.
|
109
|
+
"""Get full path or URL to a PCAP file.
|
28
110
|
|
29
111
|
Args:
|
30
112
|
pcap_file: Filename or relative path to PCAP file
|
31
113
|
|
32
114
|
Returns:
|
33
|
-
Full path to the PCAP file
|
115
|
+
Full path or URL to the PCAP file
|
34
116
|
"""
|
35
|
-
if
|
36
|
-
return
|
37
|
-
|
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)
|
38
139
|
|
39
140
|
def list_pcap_files(self) -> list[str]:
|
40
|
-
"""List all PCAP files in the configured directory.
|
141
|
+
"""List all PCAP files in the configured directory or remote URL.
|
41
142
|
|
42
143
|
Returns:
|
43
144
|
List of PCAP filenames
|
44
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
|
45
175
|
try:
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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:
|
52
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
@@ -17,7 +17,7 @@ class MCPServer:
|
|
17
17
|
config: Configuration instance
|
18
18
|
"""
|
19
19
|
self.config = config
|
20
|
-
self.mcp = FastMCP("
|
20
|
+
self.mcp = FastMCP("mcpcap")
|
21
21
|
|
22
22
|
# Initialize modules
|
23
23
|
self.dns_module = DNSModule(config)
|
@@ -37,5 +37,13 @@ class MCPServer:
|
|
37
37
|
|
38
38
|
def run(self) -> None:
|
39
39
|
"""Start the MCP server."""
|
40
|
-
|
40
|
+
import sys
|
41
|
+
|
42
|
+
# Log to stderr to avoid breaking MCP JSON-RPC protocol
|
43
|
+
source = (
|
44
|
+
self.config.pcap_url if self.config.is_remote else self.config.pcap_path
|
45
|
+
)
|
46
|
+
source_type = "remote URL" if self.config.is_remote else "directory"
|
47
|
+
print(f"Starting MCP server with PCAP {source_type}: {source}", file=sys.stderr)
|
48
|
+
|
41
49
|
self.mcp.run()
|
mcpcap/modules/dns.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""DNS analysis module."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
import tempfile
|
4
5
|
from datetime import datetime
|
5
6
|
from typing import Any
|
6
7
|
|
@@ -18,45 +19,118 @@ class DNSModule(BaseModule):
|
|
18
19
|
"""Return the name of the protocol this module analyzes."""
|
19
20
|
return "DNS"
|
20
21
|
|
21
|
-
def list_dns_packets(self, pcap_file: str = "
|
22
|
+
def list_dns_packets(self, pcap_file: str = "") -> dict[str, Any]:
|
22
23
|
"""
|
23
24
|
Analyze DNS packets from a PCAP file and return a summary of each packet.
|
24
25
|
|
25
26
|
Args:
|
26
|
-
pcap_file: Path to the PCAP file to analyze
|
27
|
+
pcap_file: Path to the PCAP file to analyze. Leave empty for direct URL remotes
|
28
|
+
or when using the first available file in local directories.
|
27
29
|
|
28
30
|
Returns:
|
29
31
|
A structured dictionary containing DNS packet analysis results
|
30
32
|
"""
|
31
|
-
#
|
32
|
-
|
33
|
+
# Handle remote files
|
34
|
+
if self.config.is_remote:
|
35
|
+
# For direct file URLs, always use the URL file (ignore pcap_file parameter)
|
36
|
+
if self.config.is_direct_file_url:
|
37
|
+
available_files = self.config.list_pcap_files()
|
38
|
+
if not available_files:
|
39
|
+
return {
|
40
|
+
"error": "No PCAP file found at the provided URL",
|
41
|
+
"pcap_url": self.config.pcap_url,
|
42
|
+
}
|
43
|
+
pcap_file = available_files[0] # Use the actual filename from URL
|
44
|
+
elif not pcap_file:
|
45
|
+
# For directory URLs, if no file specified, use the first available
|
46
|
+
available_files = self.config.list_pcap_files()
|
47
|
+
if not available_files:
|
48
|
+
return {
|
49
|
+
"error": "No PCAP files found at the provided URL",
|
50
|
+
"pcap_url": self.config.pcap_url,
|
51
|
+
"available_files": [],
|
52
|
+
}
|
53
|
+
pcap_file = available_files[0]
|
33
54
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
55
|
+
# Download remote file to temporary location
|
56
|
+
try:
|
57
|
+
with tempfile.NamedTemporaryFile(
|
58
|
+
suffix=".pcap", delete=False
|
59
|
+
) as tmp_file:
|
60
|
+
temp_path = tmp_file.name
|
61
|
+
|
62
|
+
local_path = self.config.download_pcap_file(pcap_file, temp_path)
|
63
|
+
result = self.analyze_packets(local_path)
|
64
|
+
|
65
|
+
# Clean up temporary file
|
66
|
+
try:
|
67
|
+
os.unlink(local_path)
|
68
|
+
except OSError:
|
69
|
+
pass # Ignore cleanup errors
|
70
|
+
|
71
|
+
return result
|
72
|
+
|
73
|
+
except Exception as e:
|
74
|
+
# List available PCAP files for help
|
75
|
+
available_files = self.config.list_pcap_files()
|
76
|
+
return {
|
77
|
+
"error": f"Failed to download PCAP file '{pcap_file}': {str(e)}",
|
78
|
+
"available_files": available_files,
|
79
|
+
"pcap_source": self.config.pcap_url,
|
80
|
+
}
|
81
|
+
else:
|
82
|
+
# Local file handling
|
83
|
+
if not pcap_file:
|
84
|
+
# If no file specified, use the first available local 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]
|
93
|
+
|
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
|
+
# List available PCAP files for help
|
99
|
+
available_files = self.config.list_pcap_files()
|
100
|
+
return {
|
101
|
+
"error": f"PCAP file '{pcap_file}' not found",
|
102
|
+
"available_files": available_files,
|
103
|
+
"pcap_directory": self.config.pcap_path,
|
104
|
+
}
|
43
105
|
|
44
|
-
|
106
|
+
return self.analyze_packets(full_path)
|
45
107
|
|
46
108
|
def list_pcap_files(self) -> str:
|
47
109
|
"""
|
48
|
-
List all available PCAP files in the
|
110
|
+
List all available PCAP files in the configured directory or remote URL.
|
49
111
|
|
50
112
|
Returns:
|
51
113
|
A list of available PCAP files that can be analyzed
|
52
114
|
"""
|
53
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
|
+
|
54
120
|
if files:
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
+
)
|
58
131
|
else:
|
59
|
-
|
132
|
+
source_type = "remote server" if self.config.is_remote else "directory"
|
133
|
+
return f"No PCAP files found in {source_type} {source}"
|
60
134
|
|
61
135
|
def analyze_packets(self, pcap_file: str) -> dict[str, Any]:
|
62
136
|
"""Analyze DNS packets in a PCAP file.
|
@@ -79,8 +153,15 @@ class DNSModule(BaseModule):
|
|
79
153
|
"message": "No DNS packets found in this capture",
|
80
154
|
}
|
81
155
|
|
156
|
+
# Apply max_packets limit if specified
|
157
|
+
packets_to_analyze = dns_packets
|
158
|
+
limited = False
|
159
|
+
if self.config.max_packets and len(dns_packets) > self.config.max_packets:
|
160
|
+
packets_to_analyze = dns_packets[: self.config.max_packets]
|
161
|
+
limited = True
|
162
|
+
|
82
163
|
packet_details = []
|
83
|
-
for i, pkt in enumerate(
|
164
|
+
for i, pkt in enumerate(packets_to_analyze, 1):
|
84
165
|
packet_info = self._analyze_dns_packet(pkt, i)
|
85
166
|
packet_details.append(packet_info)
|
86
167
|
|
@@ -92,10 +173,17 @@ class DNSModule(BaseModule):
|
|
92
173
|
"analysis_timestamp": datetime.now().isoformat(),
|
93
174
|
"total_packets_in_file": len(packets),
|
94
175
|
"dns_packets_found": len(dns_packets),
|
176
|
+
"dns_packets_analyzed": len(packet_details),
|
95
177
|
"statistics": stats,
|
96
178
|
"packets": packet_details,
|
97
179
|
}
|
98
180
|
|
181
|
+
# Add information about packet limiting
|
182
|
+
if limited:
|
183
|
+
result["note"] = (
|
184
|
+
f"Analysis limited to first {self.config.max_packets} DNS packets due to --max-packets setting"
|
185
|
+
)
|
186
|
+
|
99
187
|
return result
|
100
188
|
|
101
189
|
except Exception as e:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mcpcap
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: A modular Python MCP Server for analyzing PCAP files
|
5
5
|
Author: mcpcap contributors
|
6
6
|
License: MIT
|
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
24
24
|
Requires-Dist: fastmcp
|
25
25
|
Requires-Dist: scapy
|
26
|
+
Requires-Dist: requests
|
26
27
|
Provides-Extra: test
|
27
28
|
Requires-Dist: pytest; extra == "test"
|
28
29
|
Requires-Dist: pytest-cov; extra == "test"
|
@@ -85,10 +86,26 @@ uvx mcpcap
|
|
85
86
|
|
86
87
|
1. **Start the MCP Server**:
|
87
88
|
|
89
|
+
**Local PCAP file:**
|
90
|
+
```bash
|
91
|
+
mcpcap --pcap-path /path/to/specific/file.pcap
|
92
|
+
```
|
93
|
+
|
94
|
+
**Local PCAP directory:**
|
88
95
|
```bash
|
89
96
|
mcpcap --pcap-path /path/to/pcap/files
|
90
97
|
```
|
91
98
|
|
99
|
+
**Remote PCAP file:**
|
100
|
+
```bash
|
101
|
+
mcpcap --pcap-url https://example.com/sample.pcap
|
102
|
+
```
|
103
|
+
|
104
|
+
**With advanced options:**
|
105
|
+
```bash
|
106
|
+
mcpcap --pcap-path /path/to/pcaps --max-packets 100 --protocols dns
|
107
|
+
```
|
108
|
+
|
92
109
|
2. **Connect your LLM client** to the MCP server
|
93
110
|
|
94
111
|
3. **Ask questions** about your network traffic:
|
@@ -121,27 +138,80 @@ The DNS module analyzes Domain Name System packets in PCAP files.
|
|
121
138
|
|
122
139
|
### PCAP Sources
|
123
140
|
|
124
|
-
|
141
|
+
mcpcap supports multiple ways to specify PCAP data sources:
|
125
142
|
|
143
|
+
**Local PCAP File**:
|
144
|
+
```bash
|
145
|
+
mcpcap --pcap-path /local/path/to/specific.pcap
|
146
|
+
```
|
147
|
+
|
148
|
+
**Local Directory**:
|
126
149
|
```bash
|
127
150
|
mcpcap --pcap-path /local/path/to/pcaps
|
128
151
|
```
|
129
152
|
|
130
|
-
**Remote
|
153
|
+
**Remote PCAP File (Direct Link)**:
|
154
|
+
```bash
|
155
|
+
mcpcap --pcap-url https://wiki.wireshark.org/uploads/dns.cap
|
156
|
+
```
|
131
157
|
|
158
|
+
**Remote Directory Listing**:
|
132
159
|
```bash
|
133
160
|
mcpcap --pcap-url http://example.com/pcaps/
|
134
161
|
```
|
135
162
|
|
136
|
-
###
|
163
|
+
### Analysis Options
|
137
164
|
|
165
|
+
**Module Selection**:
|
138
166
|
```bash
|
139
167
|
mcpcap --modules dns --pcap-path /path/to/files
|
140
168
|
```
|
141
169
|
|
170
|
+
**Protocol Filtering**:
|
171
|
+
```bash
|
172
|
+
mcpcap --protocols dns --pcap-path /path/to/files
|
173
|
+
```
|
174
|
+
|
175
|
+
**Packet Limiting** (for large files):
|
176
|
+
```bash
|
177
|
+
mcpcap --max-packets 1000 --pcap-path /path/to/files
|
178
|
+
```
|
179
|
+
|
180
|
+
**Combined Options**:
|
181
|
+
```bash
|
182
|
+
mcpcap --pcap-path /data/capture.pcap --max-packets 500 --protocols dns
|
183
|
+
```
|
184
|
+
|
185
|
+
## CLI Reference
|
186
|
+
|
187
|
+
```bash
|
188
|
+
mcpcap [--pcap-path PATH | --pcap-url URL] [OPTIONS]
|
189
|
+
```
|
190
|
+
|
191
|
+
**Source Options** (choose one):
|
192
|
+
- `--pcap-path PATH`: Local PCAP file or directory
|
193
|
+
- `--pcap-url URL`: Remote PCAP file URL or directory listing
|
194
|
+
|
195
|
+
**Analysis Options**:
|
196
|
+
- `--modules MODULES`: Comma-separated modules to load (default: dns)
|
197
|
+
- `--protocols PROTOCOLS`: Comma-separated protocols to analyze (default: dns)
|
198
|
+
- `--max-packets N`: Maximum packets to analyze per file (default: unlimited)
|
199
|
+
|
200
|
+
**Examples**:
|
201
|
+
```bash
|
202
|
+
# Analyze specific file
|
203
|
+
mcpcap --pcap-path ./capture.pcap
|
204
|
+
|
205
|
+
# Remote file with packet limit
|
206
|
+
mcpcap --pcap-url https://example.com/dns.cap --max-packets 100
|
207
|
+
|
208
|
+
# Directory with protocol filter
|
209
|
+
mcpcap --pcap-path /captures --protocols dns --modules dns
|
210
|
+
```
|
211
|
+
|
142
212
|
## Example
|
143
213
|
|
144
|
-
An example PCAP file (`
|
214
|
+
An example PCAP file (`dns.pcap`) containing DNS traffic is included in the `examples/` directory to help you get started.
|
145
215
|
|
146
216
|
## Architecture
|
147
217
|
|
@@ -170,7 +240,26 @@ Future modules might include:
|
|
170
240
|
|
171
241
|
## Remote Access
|
172
242
|
|
173
|
-
mcpcap supports reading PCAP files from remote HTTP servers
|
243
|
+
mcpcap supports reading PCAP files from remote HTTP servers in two modes:
|
244
|
+
|
245
|
+
**Direct File Access**: Point directly to a PCAP file URL
|
246
|
+
```bash
|
247
|
+
mcpcap --pcap-url https://wiki.wireshark.org/uploads/__moin_import__/attachments/SampleCaptures/dns.cap
|
248
|
+
```
|
249
|
+
|
250
|
+
**Directory Listing**: Parse HTML directory listings to find PCAP files
|
251
|
+
```bash
|
252
|
+
mcpcap --pcap-url http://server.com/pcap-files/
|
253
|
+
```
|
254
|
+
|
255
|
+
**Supported File Types**: `.pcap`, `.pcapng`, `.cap`
|
256
|
+
|
257
|
+
**Current Limitations**:
|
258
|
+
- HTTP/HTTPS only (no authentication)
|
259
|
+
- Directory listings require standard HTML format
|
260
|
+
- Files are downloaded temporarily for analysis
|
261
|
+
|
262
|
+
Future versions may include support for Basic Authentication and other security mechanisms.
|
174
263
|
|
175
264
|
## Contributing
|
176
265
|
|
@@ -190,8 +279,10 @@ MIT
|
|
190
279
|
## Requirements
|
191
280
|
|
192
281
|
- Python 3.10+
|
193
|
-
- scapy
|
194
|
-
-
|
282
|
+
- scapy (packet parsing and analysis)
|
283
|
+
- requests (HTTP remote file access)
|
284
|
+
- fastmcp (MCP server framework)
|
285
|
+
- All dependencies are automatically installed via pip
|
195
286
|
|
196
287
|
## Support
|
197
288
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
mcpcap/__init__.py,sha256=rJwCpBXkhIvmsqHFpeR33Vg8kuipNPJ2JdlAjsTk7I4,1408
|
2
|
+
mcpcap/_version.py,sha256=5zTqm8rgXsWYBpB2M3Zw_K1D-aV8wP7NsBLrmMKkrAQ,704
|
3
|
+
mcpcap/cli.py,sha256=DflEb7i2ATn9lBx4rK43Qpf-aQjKhwSqT20BofzVrFs,2480
|
4
|
+
mcpcap/core/__init__.py,sha256=WM5GTl06ZwwqHTPiKaYB-9hwOOXe3hyHG16FshwSsjE,127
|
5
|
+
mcpcap/core/config.py,sha256=WdHYu14Cvn9C3xs3KsQ-SVRru00IH86nQfnDL57V9zE,8190
|
6
|
+
mcpcap/core/server.py,sha256=CFOwO8p_UxXHeacNbrtsKe1kbC81IG-6lJsE-1wH52s,1411
|
7
|
+
mcpcap/modules/__init__.py,sha256=iIeoZuLA-EOv0OS8WU8qDCitXJnarq9F0hA5-Y97zis,140
|
8
|
+
mcpcap/modules/base.py,sha256=3h8lGt6d6ob4SbgP6THC5PnTeMRcKfTGoJ9ZlZsQje0,826
|
9
|
+
mcpcap/modules/dns.py,sha256=cc77RxJOf-JxTLTCY8kfc_64uMawWKB3rjme9Q5H1pI,16632
|
10
|
+
mcpcap/resources/__init__.py,sha256=BPXV29wIG360w9Y9iNpQdA93H2PhT3a6CrnMZX2aaaU,109
|
11
|
+
mcpcap/resources/references.py,sha256=HCciAutgLHodlifC8goAZcWpvup3DfbVZ1rxPaXKggA,2516
|
12
|
+
mcpcap-0.3.0.dist-info/licenses/LICENSE,sha256=Ltj0zxftQyBYQMNva935v0i5QXQQOF8ygE8dQxGEtjk,1063
|
13
|
+
mcpcap-0.3.0.dist-info/METADATA,sha256=lbaIAHLHW1W4pp12et9wzGeSGnRNoaoEbxvX77rNQAc,7854
|
14
|
+
mcpcap-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
15
|
+
mcpcap-0.3.0.dist-info/entry_points.txt,sha256=ck69gPBEopmU6mzQy9P6o6ssMr89bQbrvv51IaJ50Gc,39
|
16
|
+
mcpcap-0.3.0.dist-info/top_level.txt,sha256=YkRkVGjuM3nI7cVB1l8zIAeqiS_5_vrzbUcHNkH3OXE,7
|
17
|
+
mcpcap-0.3.0.dist-info/RECORD,,
|
mcpcap-0.2.2.dist-info/RECORD
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
mcpcap/__init__.py,sha256=rJwCpBXkhIvmsqHFpeR33Vg8kuipNPJ2JdlAjsTk7I4,1408
|
2
|
-
mcpcap/_version.py,sha256=o3ZTescp-19Z9cvBGq9dQnbppljgzdUYUf98Nov0spY,704
|
3
|
-
mcpcap/cli.py,sha256=8afFamCifC44vMAAi1gNqr2c6CdBgXk33IoRgqmSH2A,1416
|
4
|
-
mcpcap/core/__init__.py,sha256=WM5GTl06ZwwqHTPiKaYB-9hwOOXe3hyHG16FshwSsjE,127
|
5
|
-
mcpcap/core/config.py,sha256=0LmnzHWWOwtHrikW2o5C9N0SLVvNuCzkNYTQQEys27U,1476
|
6
|
-
mcpcap/core/server.py,sha256=1wGGhTMLPspeH45NoVvhcnjUcLWyD6kY1u9tpt4jos4,1140
|
7
|
-
mcpcap/modules/__init__.py,sha256=iIeoZuLA-EOv0OS8WU8qDCitXJnarq9F0hA5-Y97zis,140
|
8
|
-
mcpcap/modules/base.py,sha256=3h8lGt6d6ob4SbgP6THC5PnTeMRcKfTGoJ9ZlZsQje0,826
|
9
|
-
mcpcap/modules/dns.py,sha256=NSBVfx1KWgEDFcvOpvrxIVPAXXE-SvK2E4HS3vqvlVk,12627
|
10
|
-
mcpcap/resources/__init__.py,sha256=BPXV29wIG360w9Y9iNpQdA93H2PhT3a6CrnMZX2aaaU,109
|
11
|
-
mcpcap/resources/references.py,sha256=HCciAutgLHodlifC8goAZcWpvup3DfbVZ1rxPaXKggA,2516
|
12
|
-
mcpcap-0.2.2.dist-info/licenses/LICENSE,sha256=Ltj0zxftQyBYQMNva935v0i5QXQQOF8ygE8dQxGEtjk,1063
|
13
|
-
mcpcap-0.2.2.dist-info/METADATA,sha256=DI-Qo-0o2s0_hHDsRu2g2g4eCKkeooHlyXMtF_-DSf0,5530
|
14
|
-
mcpcap-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
15
|
-
mcpcap-0.2.2.dist-info/entry_points.txt,sha256=ck69gPBEopmU6mzQy9P6o6ssMr89bQbrvv51IaJ50Gc,39
|
16
|
-
mcpcap-0.2.2.dist-info/top_level.txt,sha256=YkRkVGjuM3nI7cVB1l8zIAeqiS_5_vrzbUcHNkH3OXE,7
|
17
|
-
mcpcap-0.2.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|