Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,18 @@ utils.py Helpers: create_http_flow, replay_flows, save_flows, decode_body
- `FlowStore` assigns monotonically increasing integer IDs (`mitmproxy_mcp_id`) to each `HTTPFlow`.
- `ProxyManager.call()` is the only thread-safe way to invoke mitmproxy commands on the running event loop.
- Replay and save use mitmproxy's native commands (`replay.client`, `save.file`) rather than reimplementing logic.
- `CaptureAddon` filters flows with `capture_filter` and a runtime-updatable list of `CaptureRule` objects (`include`/`exclude`).
- `CaptureAddon` filters flows with `capture_filter` and a runtime-updatable list of `CaptureRule` objects (`include`/`exclude`). It tags each captured flow with `metadata["mitmproxy_mcp_source_proxy"]` to record which proxy instance captured it.
- `RulesAddon` runs inside the mitmproxy event loop; its rule list is protected by an `RLock` and can be updated from the MCP tool thread.
- `CryptoAddon` runs inside the mitmproxy event loop and applies user-loaded `CryptoHandler` scripts to decrypt/encrypt HTTP/WebSocket traffic.

### Auxiliary proxy (dual-proxy)

The server supports an optional auxiliary proxy (`aux_proxy_manager`) for chained proxy scenarios where encryption/decryption is split across two mitmproxy instances. Both proxies share the same `FlowStore` but have independent `CaptureAddon`, `CryptoAddon`, `RulesAddon` and `WebSocketRulesAddon` instances.

Tools that operate on flows (`flow_action` replay/resume/kill, `websocket_ctl` inject) automatically route to the correct proxy based on `flow.metadata["mitmproxy_mcp_source_proxy"]`. Other tools accept a `proxy_id` parameter (`"main"` or `"aux"`, default `"main"`) for explicit routing.

The auxiliary proxy is entirely optional — it is only used when the user calls `proxy_ctl(cmd="start", proxy_id="aux", ...)`.

## Automatic rules

The server supports automatic rules via `rule_ctl`. A rule consists of:
Expand Down Expand Up @@ -336,13 +344,14 @@ uv pip install -e ".[dev]"

| Tool | Commands |
|------|----------|
| `proxy_ctl(cmd, ...)` | `start`, `stop`, `status`, `list_options`, `clear_all`, `wireguard_config` |
| `proxy_ctl(cmd, proxy_id, ...)` | `start`, `stop`, `status`, `list_options`, `clear_all`, `wireguard_config` |
| `ca_ctl(cmd, ...)` | `status`, `export_ca`, `set_verify_upstream`, `set_upstream_ca`, `clear_upstream_ca`, `set_client_cert`, `clear_client_cert` |
| `websocket_ctl(cmd, ...)` | `list`, `get`, `inject`, `connect`, `list_rules`, `add_rule`, `delete_rule`, `clear_rules` |
| `websocket_ctl(cmd, proxy_id, ...)` | `list`, `get`, `inject`, `connect`, `list_rules`, `add_rule`, `delete_rule`, `clear_rules` |
| `http_ctl(cmd, ...)` | `list`, `get`, `delete`, `clear`, `load`, `save`, `extract_json` |
| `flow_action(action, ...)` | `replay`, `resume`, `kill`, `update`, `create`, `send` |
| `rule_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear` |
| `capture_rule_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear` |
| `crypt_ctl(cmd, proxy_id, ...)` | `list`, `load`, `unload`, `reload`, `status` |
| `rule_ctl(cmd, proxy_id, ...)` | `list`, `add`, `delete`, `clear` |
| `capture_rule_ctl(cmd, proxy_id, ...)` | `list`, `add`, `delete`, `clear` |
| `mock_server_ctl(cmd, ...)` | `start`, `add`, `stop`, `status` |
| `map_local_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear` |
| `map_remote_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear` |
Expand Down
68 changes: 63 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- **查看**: `http_ctl(cmd="list")`, `http_ctl(cmd="get")`
- **重放**: `flow_action(action="replay")`, `flow_action(action="send")` —— 基于 mitmproxy 原生 `replay.client`
- **修改**: `flow_action(action="update")`, `flow_action(action="create")`
- **辅助代理(可选)** 支持同时运行第二个 mitmproxy 实例,用于链式代理场景下的加解密分工。
- **基于 mitmproxy 自身引擎** 实现重放和保存,不重复造轮子。
- **stdio 传输**,开箱兼容 Claude Desktop。
- **SSE 传输**,可远程或网络客户端连接(Claude Code、Cursor 等)。
Expand Down Expand Up @@ -351,6 +352,63 @@ class MyHandler(CryptoHandler):
- 在 `decrypt_request` 中查询历史 handshake 流量来推导会话密钥。
- 返回 `CryptoResult(error="...")` 在 `crypt_ctl status` 中向 LLM 报告原因。

## 辅助代理(双代理链式加解密)

支持同时运行第二个 mitmproxy 实例(辅助代理),适用于链式代理场景下的加解密分工。

### 典型场景

```
客户端 → mitmA (端口 8080) → Burp/其他代理 → mitmB (端口 8082) → 服务器
```

- **mitmA**(主代理):解密客户端请求,加密返回给客户端的响应
- **mitmB**(辅助代理):加密发往服务器的请求,解密服务器响应

两个代理共享同一个 FlowStore,捕获的流量统一在 `http_ctl` 中查看。

### 使用方式

```python
# 1. 启动主代理
proxy_ctl(cmd="start", port=8080)

# 2. 启动辅助代理(不同端口)
proxy_ctl(cmd="start", proxy_id="aux", port=8082)

# 3. 分别加载加解密脚本
crypt_ctl(cmd="load", script_path="/path/to/decrypt_a.py") # 主代理
crypt_ctl(cmd="load", proxy_id="aux", script_path="/path/to/encrypt_b.py") # 辅助代理

# 4. 查看状态(自动合并显示两个代理)
proxy_ctl(cmd="status")

# 5. 停止辅助代理
proxy_ctl(cmd="stop", proxy_id="aux")
```

### 支持 proxy_id 的工具

以下工具通过 `proxy_id`(`"main"` 或 `"aux"`,默认 `"main"`)路由到指定代理:

| 工具 | 路由行为 |
|------|----------|
| `proxy_ctl` | `start`/`stop`/`clear_all`/`wireguard_config` 按 proxy_id 路由;`status` 自动合并显示 |
| `crypt_ctl` | 加解密脚本按 proxy_id 隔离,互不干扰 |
| `rule_ctl` | 自动规则按 proxy_id 隔离 |
| `capture_rule_ctl` | 捕获规则按 proxy_id 隔离 |
| `websocket_ctl` | `inject` 自动按 flow 来源路由;`connect`/规则操作按 proxy_id 路由 |

以下工具不需要 `proxy_id`,始终操作共享数据:

| 工具 | 说明 |
|------|------|
| `http_ctl` | 操作共享 FlowStore,两个代理的流量统一查看 |
| `flow_action` | `replay`/`resume`/`kill` 自动按 flow 来源路由到正确的代理 |
| `flow_action(update/create/send)` | 操作 FlowStore 数据,不涉及代理路由 |
| `mock_server_ctl` | 始终在主代理上运行 |
| `map_local_ctl` / `map_remote_ctl` | 始终在主代理上运行 |

## MCP Resources

除 tools 外,服务器还暴露一组只读的 MCP resources,客户端可以像读取文件一样直接获取状态,减少 tool 调用次数:
Expand Down Expand Up @@ -383,14 +441,14 @@ class MyHandler(CryptoHandler):

| 工具 | 命令 / 说明 |
|------|------------|
| `proxy_ctl(cmd, ...)` | `start`, `stop`, `status`, `list_options`, `clear_all`, `wireguard_config` |
| `proxy_ctl(cmd, proxy_id, ...)` | `start`, `stop`, `status`, `list_options`, `clear_all`, `wireguard_config` |
| `ca_ctl(cmd, ...)` | `status`, `export_ca`, `set_verify_upstream`, `set_upstream_ca`, `clear_upstream_ca`, `set_client_cert`, `clear_client_cert` |
| `websocket_ctl(cmd, ...)` | `list`, `get`, `inject`, `connect`, `list_rules`, `add_rule`, `delete_rule`, `clear_rules` |
| `websocket_ctl(cmd, proxy_id, ...)` | `list`, `get`, `inject`, `connect`, `list_rules`, `add_rule`, `delete_rule`, `clear_rules` |
| `http_ctl(cmd, ...)` | `list`, `get`, `delete`, `clear`, `load`, `save`, `extract_json`, `export_har`, `import_har` |
| `flow_action(action, ...)` | `replay`, `resume`, `kill`, `update`, `create`, `send` |
| `crypt_ctl(cmd, ...)` | `list`, `load`, `unload`, `reload`, `status`(用户自定义加解密脚本) |
| `rule_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear`(自动规则) |
| `capture_rule_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear`(捕获 include/exclude 规则) |
| `crypt_ctl(cmd, proxy_id, ...)` | `list`, `load`, `unload`, `reload`, `status`(用户自定义加解密脚本) |
| `rule_ctl(cmd, proxy_id, ...)` | `list`, `add`, `delete`, `clear`(自动规则) |
| `capture_rule_ctl(cmd, proxy_id, ...)` | `list`, `add`, `delete`, `clear`(捕获 include/exclude 规则) |
| `mock_server_ctl(cmd, ...)` | `start`, `add`, `stop`, `status` |
| `map_local_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear`(URL → 本地文件) |
| `map_remote_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear`(URL 重写) |
Expand Down
68 changes: 63 additions & 5 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A lightweight [Model Context Protocol (MCP)](https://modelcontextprotocol.io) se
- **View**: `http_ctl(cmd="list")`, `http_ctl(cmd="get")`
- **Replay**: `flow_action(action="replay")`, `flow_action(action="send")` — backed by mitmproxy's native `replay.client`
- **Modify**: `flow_action(action="update")`, `flow_action(action="create")`
- **Auxiliary proxy (optional)** — run a second mitmproxy instance for split encryption/decryption in chained proxy setups.
- **Built on mitmproxy's own engine** for replay and save, so we don't reinvent the wheel.
- **stdio transport** for out-of-the-box Claude Desktop compatibility.
- **SSE transport** for remote or network-based MCP clients (Claude Code, Cursor, etc.).
Expand Down Expand Up @@ -351,6 +352,63 @@ See `examples/crypto_xor_example.py` (simple XOR) and `examples/crypto_dynamic_k
- Look up a previous handshake flow in `decrypt_request` to derive a session key.
- Return `CryptoResult(error="...")` so the reason surfaces in `crypt_ctl status`.

## Auxiliary proxy (dual-proxy chained encryption)

Supports running a second mitmproxy instance (auxiliary proxy) for split encryption/decryption in chained proxy setups.

### Typical scenario

```
Client → mitmA (port 8080) → Burp/other tool → mitmB (port 8082) → Server
```

- **mitmA** (main proxy): decrypts client requests, encrypts responses back to client
- **mitmB** (auxiliary proxy): encrypts requests to server, decrypts server responses

Both proxies share the same FlowStore; all captured traffic is visible in `http_ctl`.

### Usage

```python
# 1. Start main proxy
proxy_ctl(cmd="start", port=8080)

# 2. Start auxiliary proxy (different port)
proxy_ctl(cmd="start", proxy_id="aux", port=8082)

# 3. Load encryption scripts separately
crypt_ctl(cmd="load", script_path="/path/to/decrypt_a.py") # main
crypt_ctl(cmd="load", proxy_id="aux", script_path="/path/to/encrypt_b.py") # aux

# 4. View status (auto-merged for both proxies)
proxy_ctl(cmd="status")

# 5. Stop auxiliary proxy
proxy_ctl(cmd="stop", proxy_id="aux")
```

### Tools that support proxy_id

These tools route to the specified proxy via `proxy_id` (`"main"` or `"aux"`, default `"main"`):

| Tool | Routing behavior |
|------|------------------|
| `proxy_ctl` | `start`/`stop`/`clear_all`/`wireguard_config` route by proxy_id; `status` auto-merges |
| `crypt_ctl` | Crypto scripts are isolated per proxy_id |
| `rule_ctl` | Automatic rules are isolated per proxy_id |
| `capture_rule_ctl` | Capture rules are isolated per proxy_id |
| `websocket_ctl` | `inject` auto-routes by flow source; `connect`/rules route by proxy_id |

These tools operate on shared data and do not need proxy_id:

| Tool | Notes |
|------|-------|
| `http_ctl` | Operates on shared FlowStore; traffic from both proxies is visible |
| `flow_action` | `replay`/`resume`/`kill` auto-route to the correct proxy based on flow source |
| `flow_action(update/create/send)` | Operates on FlowStore data, no proxy routing |
| `mock_server_ctl` | Always runs on the main proxy |
| `map_local_ctl` / `map_remote_ctl` | Always runs on the main proxy |

## MCP Resources

In addition to tools, the server exposes read-only MCP resources that clients can read like files, reducing the need for repeated tool calls:
Expand Down Expand Up @@ -383,14 +441,14 @@ Read mitmproxy://ca/status to see the CA/certificate configuration

| Tool | Commands / Description |
|------|------------------------|
| `proxy_ctl(cmd, ...)` | `start`, `stop`, `status`, `list_options`, `clear_all`, `wireguard_config` |
| `proxy_ctl(cmd, proxy_id, ...)` | `start`, `stop`, `status`, `list_options`, `clear_all`, `wireguard_config` |
| `ca_ctl(cmd, ...)` | `status`, `export_ca`, `set_verify_upstream`, `set_upstream_ca`, `clear_upstream_ca`, `set_client_cert`, `clear_client_cert` |
| `websocket_ctl(cmd, ...)` | `list`, `get`, `inject`, `connect`, `list_rules`, `add_rule`, `delete_rule`, `clear_rules` |
| `websocket_ctl(cmd, proxy_id, ...)` | `list`, `get`, `inject`, `connect`, `list_rules`, `add_rule`, `delete_rule`, `clear_rules` |
| `http_ctl(cmd, ...)` | `list`, `get`, `delete`, `clear`, `load`, `save`, `extract_json`, `export_har`, `import_har` |
| `flow_action(action, ...)` | `replay`, `resume`, `kill`, `update`, `create`, `send` |
| `crypt_ctl(cmd, ...)` | `list`, `load`, `unload`, `reload`, `status` (user-defined encryption/decryption scripts) |
| `rule_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear` (automatic rules) |
| `capture_rule_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear` (capture include/exclude rules) |
| `crypt_ctl(cmd, proxy_id, ...)` | `list`, `load`, `unload`, `reload`, `status` (user-defined encryption/decryption scripts) |
| `rule_ctl(cmd, proxy_id, ...)` | `list`, `add`, `delete`, `clear` (automatic rules) |
| `capture_rule_ctl(cmd, proxy_id, ...)` | `list`, `add`, `delete`, `clear` (capture include/exclude rules) |
| `mock_server_ctl(cmd, ...)` | `start`, `add`, `stop`, `status` |
| `map_local_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear` (URL → local file) |
| `map_remote_ctl(cmd, ...)` | `list`, `add`, `delete`, `clear` (URL rewrite) |
Expand Down
25 changes: 21 additions & 4 deletions src/mitmproxy_mcp/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ def __init__(
capture_filter: str | None = None,
capture_rules: list[CaptureRule] | None = None,
event_buffer: EventBuffer | None = None,
source_proxy: str = "main",
) -> None:
self.store = store
self.capture_filter = capture_filter
self._filter: flowfilter.TFilter | None = None
self._lock = threading.RLock()
self._capture_rules: list[CaptureRule] = []
self._event_buffer = event_buffer
self._source_proxy = source_proxy
self._compile_filter()
if capture_rules:
self.set_rules(capture_rules)
Expand Down Expand Up @@ -189,12 +191,18 @@ def _emit_flow_captured(self, store_id: int, flow: http.HTTPFlow) -> None:

def response(self, flow: http.HTTPFlow) -> None:
if self.should_capture(flow):
if flow.metadata is None:
flow.metadata = {}
flow.metadata["mitmproxy_mcp_source_proxy"] = self._source_proxy
store_id = self.store.add(flow)
self._emit_flow_captured(store_id, flow)

def error(self, flow: http.HTTPFlow) -> None:
# Also capture failed flows so errors are visible.
if self.should_capture(flow):
if flow.metadata is None:
flow.metadata = {}
flow.metadata["mitmproxy_mcp_source_proxy"] = self._source_proxy
store_id = self.store.add(flow)
self._emit_flow_captured(store_id, flow)

Expand All @@ -207,6 +215,9 @@ def websocket_start(self, flow: http.HTTPFlow) -> None:
# ensure the WebSocket flow is tracked in case filters behave
# differently at upgrade time.
if self.should_capture(flow):
if flow.metadata is None:
flow.metadata = {}
flow.metadata["mitmproxy_mcp_source_proxy"] = self._source_proxy
store_id = self.store.add(flow)
self._emit_flow_captured(store_id, flow)
if self._event_buffer is not None:
Expand Down Expand Up @@ -259,7 +270,7 @@ def to_options(self) -> dict[str, Any]:
class ProxyManager:
"""Manages a mitmproxy DumpMaster or WebMaster running in a background thread."""

def __init__(self, store: FlowStore) -> None:
def __init__(self, store: FlowStore, source_proxy: str = "main") -> None:
self.store = store
self._master: DumpMaster | WebMaster | None = None
self._thread: threading.Thread | None = None
Expand All @@ -270,7 +281,7 @@ def __init__(self, store: FlowStore) -> None:
self._wireguard_config: str | None = None
self._ca_config = CaConfig()
self.event_buffer = EventBuffer()
self.capture_addon = CaptureAddon(self.store, event_buffer=self.event_buffer)
self.capture_addon = CaptureAddon(self.store, event_buffer=self.event_buffer, source_proxy=source_proxy)
self.rules_addon = RulesAddon(event_buffer=self.event_buffer)
self.websocket_rules_addon = WebSocketRulesAddon(event_buffer=self.event_buffer)
self.crypto_addon = CryptoAddon(self.store, event_buffer=self.event_buffer)
Expand Down Expand Up @@ -843,17 +854,23 @@ def inject_websocket(
to_client: bool,
message: str,
binary: bool = False,
target: ProxyManager | None = None,
) -> dict[str, Any]:
"""Inject a message into an existing WebSocket connection."""
"""Inject a message into an existing WebSocket connection.

If *target* is provided, the inject command runs in that proxy's event
loop (useful when the flow was captured by a different proxy instance).
"""
flow = self.store.get(store_id)
if flow is None:
return {"success": False, "error": f"Flow with id {store_id} not found"}
if flow.websocket is None:
return {"success": False, "error": "Flow is not a WebSocket connection"}

msg_bytes = message.encode("utf-8") if not binary else base64.b64decode(message)
proxy = target or self
try:
self.call("inject.websocket", flow, to_client, msg_bytes, not binary)
proxy.call("inject.websocket", flow, to_client, msg_bytes, not binary)
except Exception as e:
return {"success": False, "error": f"Failed to inject message: {e}"}
return {"success": True, "injected": 1}
Expand Down
Loading