# DevTools MCP Server

Create an MCP (Model Context Protocol) server that exposes your DevTools traffic data to AI assistants. This enables Codex CLI, ChatGPT, and Claude Code to query your network traffic directly.

{% hint style="info" %}
**Looking for local-only?** See the [Ollama guide](https://docs.qpoint.io/guides/devtools-guides/ai-troubleshooting-with-devtools) for a fully local setup without external dependencies.
{% endhint %}

**What you'll build:**

```
You: "What external APIs is my app calling?"

AI: [Calling get_hosts tool...]
"Your application is connecting to 16 external APIs:
- api.stripe.com (3 requests, all 200s) - /v1/customers, /v1/charges
- api.openai.com (2 requests, 401 errors) - /v1/chat/completions
- api.segment.io (2 requests, all 200s) - /v1/track
..."

You: "Are we leaking any sensitive data?"

AI: [Calling check_sensitive_data tool...]
"Found 5 requests with sensitive data:
- api.github.com: X-Api-Key header exposed
- httpbin.org: Password in request body
- www.googleapis.com: Database credentials in payload
..."
```

***

## Available Tools

| Tool                   | Use Case                              | Default Capture    |
| ---------------------- | ------------------------------------- | ------------------ |
| `get_traffic`          | Full HTTP details for debugging       | 60s, 500 requests  |
| `get_traffic_summary`  | Quick overview of traffic patterns    | 60s, 1000 requests |
| `get_hosts`            | Map all external API dependencies     | 60s, 1000 requests |
| `get_errors`           | Debug 4xx/5xx failures                | 60s, 1000 requests |
| `check_sensitive_data` | Security audit for leaked secrets     | 60s, 500 requests  |
| `get_processes`        | Which process/container made requests | 60s, 1000 requests |
| `get_payloads`         | Inspect request/response bodies       | 60s, 500 requests  |
| `search_traffic`       | Filter by host, method, status        | 60s, 1000 requests |
| `get_connections`      | Network/TLS connection details        | 60s, 2000 events   |

All tools support up to **5 minutes** of capture with `seconds=300`.

***

## Prerequisites

* Linux host with Qtap installed and DevTools enabled
* Python 3.10+

***

## Create the MCP Server

### Step 1: Install Dependencies

```bash
pip install mcp httpx
```

### Step 2: Create the Server

Save this as `devtools_mcp.py`:

```python
#!/usr/bin/env python3
"""
MCP Server that exposes Qtap DevTools traffic data to AI assistants.
Works with Codex CLI, ChatGPT, and Claude Code.

Tools:
- get_traffic: Full HTTP transactions with headers and bodies
- get_traffic_summary: Quick overview of traffic patterns
- check_sensitive_data: Scan for API keys, tokens, passwords
- get_errors: All 4xx/5xx responses with full details
- get_hosts: External API dependency mapping
- get_connections: Active network connections
- get_processes: Process/container attribution
- get_payloads: Request/response bodies for data inspection
- search_traffic: Filter traffic by host, path, status, method
"""

import base64
import json
import re
import time
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Qtap DevTools")

DEVTOOLS_URL = "http://localhost:10001/devtools/api/events"

# Sensitive data patterns for security scanning
SENSITIVE_PATTERNS = [
    (r'[Aa]uthorization', 'Authorization header'),
    (r'[Bb]earer\s+[A-Za-z0-9\-_\.]+', 'Bearer token'),
    (r'[Aa]pi[-_]?[Kk]ey', 'API key'),
    (r'[Ss]ecret', 'Secret'),
    (r'[Pp]assword', 'Password'),
    (r'[Tt]oken', 'Token'),
    (r'[Cc]ookie', 'Cookie'),
    (r'[Ss]ession[-_]?[Ii]d', 'Session ID'),
    (r'[Cc]redential', 'Credential'),
    (r'[Pp]rivate[-_]?[Kk]ey', 'Private key'),
    (r'sk-[a-zA-Z0-9]{20,}', 'OpenAI API key'),
    (r'ghp_[a-zA-Z0-9]{36}', 'GitHub PAT'),
    (r'xox[baprs]-[a-zA-Z0-9-]+', 'Slack token'),
]


def _capture_raw_events(seconds: int = 30, max_events: int = 500):
    """Capture raw events from DevTools SSE stream."""
    seconds = min(seconds, 300)  # Up to 5 minutes

    try:
        with httpx.Client(timeout=seconds + 10) as client:
            events = []
            current_event_type = None
            first_event_time = None

            with client.stream("GET", DEVTOOLS_URL, timeout=seconds + 10) as response:
                start = time.time()
                for line in response.iter_lines():
                    if time.time() - start > seconds:
                        break

                    if line.startswith("event: "):
                        current_event_type = line[7:]
                    elif line.startswith("data: "):
                        try:
                            event_data = json.loads(line[6:])
                            event_ts = event_data.get("ts", "")

                            if first_event_time is None:
                                first_event_time = event_ts

                            # Skip initial process dump
                            if current_event_type == "process.started":
                                if event_ts[:20] == first_event_time[:20]:
                                    continue

                            events.append({
                                "type": current_event_type,
                                "ts": event_ts,
                                "data": event_data.get("data", {})
                            })
                        except json.JSONDecodeError:
                            pass

                    if len(events) >= max_events:
                        break

            return events

    except httpx.ConnectError:
        return {"error": "Cannot connect to DevTools API at localhost:10001",
                "hint": "Start Qtap with --enable-dev-tools or ENABLE_DEV_TOOLS=true"}
    except Exception as e:
        return {"error": str(e)}


def _decode_http_transaction(event):
    """Decode a raw http_transaction event into structured data."""
    inner = event.get("data", {})
    if isinstance(inner, dict) and "data" in inner:
        payload = inner.get("data")
        if isinstance(payload, dict) and "data" in payload:
            payload = payload.get("data")
        if not isinstance(payload, str):
            return None
        try:
            decoded = json.loads(base64.b64decode(payload))

            # Decode response body
            res = decoded.get("response", {})
            if res.get("body"):
                try:
                    res["body"] = base64.b64decode(res["body"]).decode('utf-8', errors='replace')
                except:
                    res["body"] = "[binary data]"

            # Decode request body if present
            req = decoded.get("request", {})
            if req.get("body"):
                try:
                    req["body"] = base64.b64decode(req["body"]).decode('utf-8', errors='replace')
                except:
                    req["body"] = "[binary data]"

            return {
                "ts": event.get("ts"),
                "metadata": decoded.get("metadata", {}),
                "request": req,
                "response": res
            }
        except:
            pass
    return None


def _capture_http(seconds: int = 30, max_http: int = 500):
    """Capture and decode HTTP transactions."""
    events = _capture_raw_events(seconds=seconds, max_events=2000)

    if isinstance(events, dict) and "error" in events:
        return events

    http_events = []
    for event in events:
        if event.get("type") == "request.http_transaction":
            decoded = _decode_http_transaction(event)
            if decoded:
                http_events.append(decoded)
                if len(http_events) >= max_http:
                    break

    return http_events


# =============================================================================
# HTTP Traffic Tools
# =============================================================================

@mcp.tool()
def get_traffic(seconds: int = 60, max_requests: int = 500) -> list | dict:
    """
    Capture HTTP traffic with full request/response details.

    Best for: Debugging specific requests, seeing exactly what's being sent/received.

    Args:
        seconds: Capture duration (default 60, max 300 = 5 minutes)
        max_requests: Max HTTP requests to return (default 500, max 1000)

    Returns: JSON array of HTTP transactions with method, URL, headers, and bodies.
    """
    max_requests = min(max_requests, 1000)
    events = _capture_http(seconds=seconds, max_http=max_requests)

    if isinstance(events, dict) and "error" in events:
        return events

    if not events:
        return {"message": "No HTTP traffic captured", "hint": "Try increasing capture time or generate some traffic"}

    return events


@mcp.tool()
def get_traffic_summary(seconds: int = 60) -> dict:
    """
    Quick overview of HTTP traffic patterns.

    Best for: "What's happening right now?", "How much traffic?", "Any errors?"

    Args:
        seconds: Capture duration (default 60, max 300 = 5 minutes)

    Returns: Host counts, method breakdown, status code distribution, error list.
    """
    events = _capture_http(seconds=seconds, max_http=1000)

    if isinstance(events, dict) and "error" in events:
        return events

    hosts = {}
    status_codes = {}
    methods = {}
    errors = []

    for event in events:
        req = event.get("request", {})
        res = event.get("response", {})

        host = req.get("authority", "unknown")
        method = req.get("method", "?")
        status = res.get("status", 0)

        hosts[host] = hosts.get(host, 0) + 1
        methods[method] = methods.get(method, 0) + 1
        status_codes[str(status)] = status_codes.get(str(status), 0) + 1

        if status >= 400:
            errors.append({
                "host": host,
                "method": method,
                "path": req.get("path", "/")[:60],
                "status": status
            })

    return {
        "total_requests": len(events),
        "unique_hosts": len(hosts),
        "hosts": dict(sorted(hosts.items(), key=lambda x: -x[1])[:25]),
        "methods": methods,
        "status_codes": dict(sorted(status_codes.items())),
        "error_count": len(errors),
        "errors": errors[:30]
    }


@mcp.tool()
def search_traffic(
    seconds: int = 60,
    host: str = None,
    path_contains: str = None,
    method: str = None,
    status_min: int = None,
    status_max: int = None,
    max_results: int = 200
) -> dict:
    """
    Search and filter HTTP traffic.

    Best for: "Show me requests to api.stripe.com", "Find all POST requests", "Show 500 errors"

    Args:
        seconds: Capture duration (default 60, max 300)
        host: Filter by host (partial match)
        path_contains: Filter by path substring
        method: Filter by HTTP method (GET, POST, etc.)
        status_min: Minimum status code
        status_max: Maximum status code
        max_results: Max results to return (default 200)

    Returns: Filtered HTTP transactions matching criteria.
    """
    events = _capture_http(seconds=seconds, max_http=1000)

    if isinstance(events, dict) and "error" in events:
        return events

    filtered = []
    for event in events:
        req = event.get("request", {})
        res = event.get("response", {})

        if host and host.lower() not in req.get("authority", "").lower():
            continue
        if path_contains and path_contains.lower() not in req.get("path", "").lower():
            continue
        if method and req.get("method", "").upper() != method.upper():
            continue
        if status_min and res.get("status", 0) < status_min:
            continue
        if status_max and res.get("status", 0) > status_max:
            continue

        filtered.append(event)

    return {
        "total_scanned": len(events),
        "matches": len(filtered),
        "requests": filtered[:max_results]
    }


# =============================================================================
# Security Tools
# =============================================================================

@mcp.tool()
def check_sensitive_data(seconds: int = 60) -> dict:
    """
    Scan traffic for sensitive data: API keys, tokens, passwords, credentials.

    Best for: "Are we leaking secrets?", "What sensitive data is being transmitted?"

    Args:
        seconds: Capture duration (default 60, max 300 = 5 minutes)

    Returns: Report of sensitive patterns found in headers and bodies.
    """
    events = _capture_http(seconds=seconds, max_http=500)

    if isinstance(events, dict) and "error" in events:
        return events

    findings = []

    for event in events:
        req = event.get("request", {})
        res = event.get("response", {})
        host = req.get("authority", "unknown")
        path = req.get("path", "/")

        event_findings = []

        # Check request headers
        for header, value in req.get("headers", {}).items():
            for pattern, label in SENSITIVE_PATTERNS:
                if re.search(pattern, header, re.IGNORECASE) or re.search(pattern, str(value)):
                    event_findings.append({
                        "location": f"request header: {header}",
                        "type": label,
                        "preview": str(value)[:80] + ("..." if len(str(value)) > 80 else "")
                    })
                    break

        # Check response headers
        for header, value in res.get("headers", {}).items():
            for pattern, label in SENSITIVE_PATTERNS:
                if re.search(pattern, header, re.IGNORECASE):
                    event_findings.append({
                        "location": f"response header: {header}",
                        "type": label
                    })
                    break

        # Check request body
        req_body = str(req.get("body", ""))
        if req_body:
            for pattern, label in SENSITIVE_PATTERNS:
                match = re.search(f'.{{0,30}}{pattern}.{{0,30}}', req_body, re.IGNORECASE)
                if match:
                    event_findings.append({
                        "location": "request body",
                        "type": label,
                        "context": match.group(0)
                    })

        # Check response body
        res_body = str(res.get("body", ""))
        if res_body and len(res_body) < 50000:
            for pattern, label in SENSITIVE_PATTERNS:
                match = re.search(f'.{{0,30}}{pattern}.{{0,30}}', res_body, re.IGNORECASE)
                if match:
                    event_findings.append({
                        "location": "response body",
                        "type": label,
                        "context": match.group(0)
                    })

        if event_findings:
            findings.append({
                "host": host,
                "method": req.get("method"),
                "path": path[:100],
                "findings": event_findings
            })

    return {
        "requests_scanned": len(events),
        "requests_with_sensitive_data": len(findings),
        "risk_level": "HIGH" if len(findings) > 5 else "MEDIUM" if len(findings) > 0 else "LOW",
        "findings": findings
    }


# =============================================================================
# Error Analysis Tools
# =============================================================================

@mcp.tool()
def get_errors(seconds: int = 60) -> dict:
    """
    Get all HTTP errors (4xx and 5xx) with full details for debugging.

    Best for: "What's failing?", "Why are requests erroring?", "Debug 500 errors"

    Args:
        seconds: Capture duration (default 60, max 300 = 5 minutes)

    Returns: Failed requests with status, headers, and response bodies.
    """
    events = _capture_http(seconds=seconds, max_http=1000)

    if isinstance(events, dict) and "error" in events:
        return events

    errors = []
    for event in events:
        res = event.get("response", {})
        status = res.get("status", 0)

        if status >= 400:
            req = event.get("request", {})
            errors.append({
                "ts": event.get("ts"),
                "host": req.get("authority"),
                "method": req.get("method"),
                "path": req.get("path"),
                "url": req.get("url"),
                "status": status,
                "request_headers": req.get("headers", {}),
                "request_body": req.get("body", "")[:1000] if req.get("body") else None,
                "response_headers": res.get("headers", {}),
                "response_body": res.get("body", "")[:3000] if res.get("body") else None
            })

    by_status = {}
    for err in errors:
        status = str(err["status"])
        by_status[status] = by_status.get(status, 0) + 1

    return {
        "total_requests": len(events),
        "error_count": len(errors),
        "by_status": by_status,
        "errors": errors[:200]
    }


# =============================================================================
# Infrastructure Tools
# =============================================================================

@mcp.tool()
def get_hosts(seconds: int = 60) -> dict:
    """
    Map all external hosts/APIs being called.

    Best for: "What external services do we depend on?", "API inventory"

    Args:
        seconds: Capture duration (default 60, max 300 = 5 minutes)

    Returns: List of hosts with request counts, methods, status codes, sample paths.
    """
    events = _capture_http(seconds=seconds, max_http=1000)

    if isinstance(events, dict) and "error" in events:
        return events

    hosts = {}
    for event in events:
        req = event.get("request", {})
        res = event.get("response", {})
        host = req.get("authority", "unknown")

        if host not in hosts:
            hosts[host] = {
                "count": 0,
                "methods": set(),
                "status_codes": set(),
                "paths": [],
                "errors": 0
            }

        hosts[host]["count"] += 1
        hosts[host]["methods"].add(req.get("method", "?"))
        hosts[host]["status_codes"].add(res.get("status", 0))
        if res.get("status", 0) >= 400:
            hosts[host]["errors"] += 1
        if len(hosts[host]["paths"]) < 5:
            hosts[host]["paths"].append(req.get("path", "/")[:60])

    result = {}
    for host, data in sorted(hosts.items(), key=lambda x: -x[1]["count"]):
        result[host] = {
            "requests": data["count"],
            "errors": data["errors"],
            "methods": sorted(data["methods"]),
            "status_codes": sorted(data["status_codes"]),
            "sample_paths": data["paths"]
        }

    return {
        "capture_seconds": seconds,
        "total_requests": len(events),
        "unique_hosts": len(result),
        "hosts": result
    }


@mcp.tool()
def get_connections(seconds: int = 60) -> dict:
    """
    Get active network connections with TLS and protocol info.

    Best for: "What connections are open?", "TLS versions?", "Connection issues?"

    Args:
        seconds: Capture duration (default 60, max 300 = 5 minutes)

    Returns: Connection details including remote addresses, TLS info, and states.
    """
    events = _capture_raw_events(seconds=seconds, max_events=2000)

    if isinstance(events, dict) and "error" in events:
        return events

    connections = {}

    for event in events:
        event_type = event.get("type", "")
        data = event.get("data", {})

        if event_type in ["connection.opened", "connection.updated", "connection.closed"]:
            conn_id = data.get("connectionId") or data.get("id")
            if not conn_id:
                continue

            if conn_id not in connections:
                connections[conn_id] = {
                    "id": conn_id,
                    "events": [],
                    "remote": None,
                    "local": None,
                    "tls": None,
                    "protocol": None
                }

            connections[conn_id]["events"].append(event_type.split(".")[-1])

            if data.get("remote"):
                connections[conn_id]["remote"] = data["remote"]
            if data.get("local"):
                connections[conn_id]["local"] = data["local"]
            if data.get("tls"):
                connections[conn_id]["tls"] = data["tls"]
            if data.get("protocol"):
                connections[conn_id]["protocol"] = data["protocol"]

    conn_list = list(connections.values())

    return {
        "capture_seconds": seconds,
        "total_connections": len(conn_list),
        "connections": conn_list[:200]
    }


@mcp.tool()
def get_processes(seconds: int = 60) -> dict:
    """
    Get processes making network requests with container attribution.

    Best for: "Which process is calling X?", "What's this container doing?"

    Args:
        seconds: Capture duration (default 60, max 300 = 5 minutes)

    Returns: Process info including executable path, container name, and request counts.
    """
    events = _capture_http(seconds=seconds, max_http=1000)

    if isinstance(events, dict) and "error" in events:
        return events

    processes = {}

    for event in events:
        meta = event.get("metadata", {})
        req = event.get("request", {})

        proc_exe = meta.get("process_exe", "unknown")
        proc_id = meta.get("process_id", "?")

        key = proc_exe
        if key not in processes:
            processes[key] = {
                "executable": proc_exe,
                "process_ids": set(),
                "request_count": 0,
                "hosts_called": set(),
                "methods": set()
            }

        processes[key]["process_ids"].add(proc_id)
        processes[key]["request_count"] += 1
        processes[key]["hosts_called"].add(req.get("authority", "unknown"))
        processes[key]["methods"].add(req.get("method", "?"))

    result = []
    for proc in sorted(processes.values(), key=lambda x: -x["request_count"]):
        result.append({
            "executable": proc["executable"],
            "process_ids": sorted(proc["process_ids"])[:5],
            "requests": proc["request_count"],
            "hosts": sorted(proc["hosts_called"])[:10],
            "methods": sorted(proc["methods"])
        })

    return {
        "capture_seconds": seconds,
        "total_requests": len(events),
        "unique_processes": len(result),
        "processes": result[:30]
    }


# =============================================================================
# Payload Inspection Tools
# =============================================================================

@mcp.tool()
def get_payloads(seconds: int = 60, include_responses: bool = True, max_results: int = 200) -> dict:
    """
    Get request and response bodies for data inspection.

    Best for: "What data is being sent?", "Inspect API payloads", "Debug request bodies"

    Args:
        seconds: Capture duration (default 60, max 300 = 5 minutes)
        include_responses: Include response bodies (default True)
        max_results: Max payloads to return (default 200)

    Returns: Request/response bodies with metadata.
    """
    events = _capture_http(seconds=seconds, max_http=500)

    if isinstance(events, dict) and "error" in events:
        return events

    payloads = []
    for event in events:
        req = event.get("request", {})
        res = event.get("response", {})

        req_body = req.get("body")
        res_body = res.get("body") if include_responses else None

        if not req_body and not res_body:
            continue

        payload = {
            "host": req.get("authority"),
            "method": req.get("method"),
            "path": req.get("path", "/")[:100],
            "status": res.get("status"),
            "content_type": res.get("content_type")
        }

        if req_body:
            payload["request_body"] = req_body[:5000]
            payload["request_body_size"] = len(req_body)

        if res_body:
            payload["response_body"] = res_body[:10000]
            payload["response_body_size"] = len(res_body)

        payloads.append(payload)

    return {
        "capture_seconds": seconds,
        "requests_with_bodies": len(payloads),
        "payloads": payloads[:max_results]
    }


if __name__ == "__main__":
    mcp.run()
```

Make it executable:

```bash
chmod +x devtools_mcp.py
```

***

## Connect to AI Assistants

### Option 1: Codex CLI

[Codex CLI](https://github.com/openai/codex) is OpenAI's terminal-based coding assistant that supports MCP servers natively.

**Add the MCP server to your config:**

```bash
codex mcp add devtools -- python3 /path/to/devtools_mcp.py
```

Or edit `~/.codex/config.toml` directly:

```toml
[mcp_servers.devtools]
command = "python3"
args = ["/path/to/devtools_mcp.py"]
```

{% hint style="info" %}
**Using a virtual environment?** Replace `python3` with your venv Python path:

```bash
codex mcp add devtools -- /path/to/venv/bin/python /path/to/devtools_mcp.py
```

{% endhint %}

**Use it:**

```bash
codex
> What external APIs is my application calling? Use the devtools server.
```

Codex will call `get_hosts` and analyze the results.

***

### Option 2: ChatGPT Developer Mode

ChatGPT can connect to MCP servers via Developer Mode connectors. Since ChatGPT requires HTTPS, you'll need to expose your local server.

**1. Start the MCP server with HTTP transport:**

```python
# Add to devtools_mcp.py, replace the if __name__ block:
if __name__ == "__main__":
    mcp.run(transport="http", host="0.0.0.0", port=8080)
```

**2. Expose your local server via HTTPS tunnel:**

{% tabs %}
{% tab title="Cloudflared (Recommended)" %}

```bash
# One-liner, no account required
cloudflared tunnel --url http://localhost:8080
```

This creates a temporary `*.trycloudflare.com` URL you can use immediately.
{% endtab %}

{% tab title="ngrok" %}

```bash
ngrok http 8080
```

Requires a free ngrok account.
{% endtab %}
{% endtabs %}

**3. In ChatGPT:**

* Go to Settings > Connectors > Advanced Settings
* Enable Developer Mode
* Add a new connector with your tunnel URL

***

### Option 3: Claude Code

Claude Code supports MCP servers natively via the CLI.

**Add the MCP server:**

```bash
claude mcp add --transport stdio devtools -- python3 /path/to/devtools_mcp.py
```

{% hint style="info" %}
**Using a virtual environment?** Replace `python3` with your venv Python path:

```bash
claude mcp add --transport stdio devtools -- /path/to/venv/bin/python /path/to/devtools_mcp.py
```

{% endhint %}

**Verify it's connected:**

```bash
claude mcp list
# Should show: devtools: ... - ✓ Connected
```

**Use it in Claude Code:**

```
What external APIs is my app calling?
```

Claude Code will automatically use the appropriate tools to analyze your traffic.

***

## Example Prompts

Once connected, try these prompts:

**Quick Overview:**

* "What's happening with my network traffic?"
* "Give me a summary of all HTTP requests"
* "How many errors are there?"

**API Discovery:**

* "What external APIs is my app calling?"
* "Which services are we depending on?"
* "Show me all the hosts we're connecting to"

**Security Audit:**

* "Are we leaking any sensitive data?"
* "Check for exposed API keys or passwords"
* "What credentials are being transmitted?"

**Error Debugging:**

* "What's failing? Show me all errors"
* "Why are requests to stripe.com failing?"
* "Debug the 401 errors"

**Process Attribution:**

* "Which process is calling the OpenAI API?"
* "What requests is the Python process making?"
* "Show me traffic by container"

**Payload Inspection:**

* "What data is being sent to external services?"
* "Show me the request bodies"
* "What's in the response from api.github.com?"

**Filtered Search:**

* "Show me all POST requests"
* "Find requests to anything with 'stripe' in the host"
* "Show only 500 errors"

***

## Example Output

### get\_hosts()

```json
{
  "capture_seconds": 60,
  "total_requests": 25,
  "unique_hosts": 16,
  "hosts": {
    "api.stripe.com": {
      "requests": 3,
      "errors": 0,
      "methods": ["GET", "POST"],
      "status_codes": [200],
      "sample_paths": ["/v1/customers", "/v1/charges", "/v1/balance"]
    },
    "api.openai.com": {
      "requests": 2,
      "errors": 2,
      "methods": ["POST"],
      "status_codes": [401],
      "sample_paths": ["/v1/chat/completions"]
    }
  }
}
```

### check\_sensitive\_data()

```json
{
  "requests_scanned": 25,
  "requests_with_sensitive_data": 5,
  "risk_level": "MEDIUM",
  "findings": [
    {
      "host": "api.github.com",
      "method": "GET",
      "path": "/user",
      "findings": [
        {
          "location": "request header: Authorization",
          "type": "Authorization header",
          "preview": "Bearer ghp_xxxxxxxxxxxx..."
        }
      ]
    }
  ]
}
```

***

## Troubleshooting

### "Cannot connect to DevTools API"

Ensure Qtap is running with DevTools enabled:

{% tabs %}
{% tab title="Binary" %}

```bash
sudo qtap --enable-dev-tools
```

{% endtab %}

{% tab title="Docker" %}

```bash
docker run -d --name qtap \
  --user 0:0 --privileged \
  --cap-add CAP_BPF --cap-add CAP_SYS_ADMIN \
  --pid=host --network=host \
  -v /sys:/sys \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -e TINI_SUBREAPER=1 \
  -e ENABLE_DEV_TOOLS=true \
  --ulimit=memlock=-1 \
  us-docker.pkg.dev/qpoint-edge/public/qtap:v0
```

{% endtab %}
{% endtabs %}

Verify it's accessible:

```bash
curl -s http://localhost:10001/devtools/api/events | head -3
```

### "MCP server not found" in Codex

Check your config path:

```bash
cat ~/.codex/config.toml
```

Ensure the Python path is absolute and the script exists.

### "No HTTP traffic captured"

* Ensure traffic is being generated during the capture window
* Try increasing the capture time: `get_traffic(seconds=120)`
* Check that Qtap is capturing HTTP (not just connections)

### ChatGPT can't reach the server

* Verify cloudflared/ngrok is running and the URL is correct
* Check that the MCP server is listening on the right port
* Ensure no firewall is blocking the connection

***

## Next Steps

* [Traffic Analysis MCP Server](https://docs.qpoint.io/guides/devtools-guides/traffic-analysis-mcp-server) - Type inference and test generation from traffic
* [DevTools API Reference](https://docs.qpoint.io/guides/devtools-guides/devtools-api) - Full API documentation
* [Ollama Guide](https://docs.qpoint.io/guides/devtools-guides/ai-troubleshooting-with-devtools) - Local-only setup
* [DevTools Interface Guide](https://docs.qpoint.io/guides/devtools-guides/devtools-interface-guide) - Browser UI
