Skip to content

Commit 5cba569

Browse files
authored
Merge pull request #9 from sleeyax/feat/load-traffic-file
feat: add `load_traffic_file` tool for importing HAR and flow files
2 parents 8c6bd98 + a6b989d commit 5cba569

2 files changed

Lines changed: 98 additions & 1 deletion

File tree

src/mitmproxy_mcp/core/recorder.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import json
2+
import os
23
import shlex
34
import sqlite3
45
import sys
5-
from collections import deque # added
6+
from collections import deque
67
from typing import Any, Dict, List, Optional
8+
from urllib.parse import urlparse
79

810
from mitmproxy import http
11+
from mitmproxy.io import FlowReader
912

1013
from .scope import ScopeManager
1114
from .utils import get_safe_text
@@ -285,6 +288,64 @@ def get_by_ids(self, flow_ids: List[str]) -> List[Dict[str, Any]]:
285288
)
286289
return results
287290

291+
def import_from_file(
292+
self,
293+
file_path: str,
294+
append: bool = False,
295+
scope: Optional[List[str]] = None,
296+
) -> Dict[str, Any]:
297+
"""Import flows from a HAR or mitmproxy flow file.
298+
299+
Uses mitmproxy's FlowReader which auto-detects format (HAR if JSON,
300+
native tnetstring otherwise).
301+
302+
Args:
303+
file_path: Path to .har or .mitm/.flow file.
304+
append: If False, clear existing traffic before import.
305+
scope: Optional list of domains to filter by during import.
306+
307+
Returns:
308+
Dict with import stats: {"imported": int, "skipped": int, "errors": int}
309+
"""
310+
if not append:
311+
self.clear()
312+
313+
stats = {"imported": 0, "skipped": 0, "errors": 0}
314+
315+
if not os.path.exists(file_path):
316+
print(f"File not found: {file_path}", file=sys.stderr)
317+
return stats
318+
319+
allowed_exts = ('.har', '.mitm', '.flow')
320+
if not any(str(file_path).lower().endswith(ext) for ext in allowed_exts):
321+
print(f"Unsupported file extension: {file_path}", file=sys.stderr)
322+
return stats
323+
324+
with open(file_path, "rb") as f:
325+
reader = FlowReader(f)
326+
for flow in reader.stream():
327+
try:
328+
if not isinstance(flow, http.HTTPFlow):
329+
stats["skipped"] += 1
330+
continue
331+
332+
if scope:
333+
host = urlparse(flow.request.url).hostname or ""
334+
if not any(host == d or host.endswith("." + d) for d in scope):
335+
stats["skipped"] += 1
336+
continue
337+
338+
self.save_flow(flow)
339+
stats["imported"] += 1
340+
except Exception as e:
341+
stats["errors"] += 1
342+
print(
343+
f"Skipped flow during import: {e}",
344+
file=sys.stderr,
345+
)
346+
347+
return stats
348+
288349
def _generate_curl(self, request: SimpleRequest) -> str:
289350
try:
290351
cmd = ["curl", "-X", request.method]

src/mitmproxy_mcp/core/server.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,42 @@ async def inspect_flow(flow_id: str) -> str:
249249
return json.dumps(data, indent=2)
250250

251251

252+
@mcp.tool()
253+
async def load_traffic_file(
254+
file_path: str,
255+
append: bool = False,
256+
scope: str = None,
257+
) -> str:
258+
"""
259+
Import flows from a HAR or mitmproxy flow file into the traffic database.
260+
After import, all traffic inspection tools work on the imported data.
261+
No proxy needs to be running.
262+
Args:
263+
file_path: Path to .har or .mitm/.flow file
264+
append: If True, keep existing traffic. If False (default), clear first.
265+
scope: Comma-separated list of domains to filter by during import.
266+
Only flows matching these domains are imported.
267+
"""
268+
scope_list = (
269+
[d.strip() for d in scope.split(",") if d.strip()] if scope else None
270+
)
271+
try:
272+
stats = await asyncio.to_thread(
273+
controller.recorder.db.import_from_file,
274+
file_path, append=append, scope=scope_list
275+
)
276+
return json.dumps(
277+
{
278+
"status": "ok",
279+
"imported": stats["imported"],
280+
"skipped": stats["skipped"],
281+
"errors": stats["errors"],
282+
}
283+
)
284+
except Exception as e:
285+
return json.dumps({"status": "error", "message": str(e)})
286+
287+
252288
@mcp.tool()
253289
async def extract_from_flow(flow_id: str, json_path: str = None, css_selector: str = None) -> str:
254290
"""

0 commit comments

Comments
 (0)