diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb10195..8edfe65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index dbef113..83eebd7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![CI](https://github.com/tech4242/mcphawk/actions/workflows/ci.yml/badge.svg)](https://github.com/tech4242/mcphawk/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/tech4242/mcphawk/branch/main/graph/badge.svg)](https://codecov.io/gh/tech4242/mcphawk) - [![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) + [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![FastAPI](https://img.shields.io/badge/FastAPI-005571?style=flat&logo=fastapi)](https://fastapi.tiangolo.com/) [![Vue.js](https://img.shields.io/badge/vue.js-3.x-brightgreen.svg)](https://vuejs.org/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) @@ -11,38 +11,67 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -MCPHawk is a passive sniffer for **Model Context Protocol (MCP)** traffic, similar to Wireshark but MCP-focused. It's Wireshark x mcpinspector. - -- It captures JSON-RPC traffic between MCP clients and WebSocket/TCP-based MCP servers (IPv4 and IPv6) e.g. from any tool, agent, or LLM -- MCPHawk can reconstruct full JSON-RPC messages from raw TCP traffic without requiring a handshake. -- It captures traffic "on the wire" between any MCP client and serverβ€”does not require client/server modification. - -MCPHawk Logo - -## Features - -Non-exhaustive list: -- **Proper JSON-RPC 2.0 message type detection**: - - Requests (method + id) - - Responses (result/error + id) - - Notifications (method without id) - - Error responses -- **Auto-detect mode** - automatically discovers MCP traffic on any port without prior configuration -- **Flexible traffic filtering**: - - Monitor specific ports with `--port` - - Use custom BPF filters with `--filter` - - Auto-detect MCP traffic on all ports with `--auto-detect` -- **Chronological message display** - messages shown in order as captured -- **Message filtering** - view all, requests only, responses only, or notifications only -- **Optional ID-based pairing visualization** - see which requests and responses belong together -- **Real-time statistics** - message counts by type -- **Console-only mode** - use `mcphawk sniff` for terminal output without web UI -- **Historical log viewing** - use `mcphawk web --no-sniffer` to view past captures without active sniffing -- **Chill UX** - - dark mode 🌝 - - expand mode to directly see JSON withtout detailed view - - filtering - - always see if WS connection is up for live updates +MCPHawk is a passive network analyzer for **Model Context Protocol (MCP)** traffic, providing deep visibility into MCP client-server interactions. Think Wireshark meets mcpinspector, purpose-built for the MCP ecosystem. + +**Key Capabilities:** +- **Protocol-Aware Capture**: Understands MCP's JSON-RPC 2.0 transport layer, capturing and reassembling messages from raw TCP streams +- **Transport Agnostic**: Monitors MCP traffic across all standard transports +- **Zero-Configuration Monitoring**: Passively observes MCP communication without proxies, certificates, or modifications to clients/servers +- **Full Message Reconstruction**: Advanced TCP stream reassembly handles fragmented packets, chunked HTTP transfers, and SSE streams + +MCPHawk Screenshot + +## Core Features + +### πŸ” MCP Protocol Analysis +- **Complete JSON-RPC 2.0 Support**: Correctly identifies and categorizes all MCP message types + - **Requests**: Method calls with unique IDs for correlation + - **Responses**: Success results and error responses with matching IDs + - **Notifications**: Fire-and-forget method calls without IDs + - **Batch Operations**: Support for JSON-RPC batch requests/responses +- **Transport-Specific Handling**: + - **HTTP/SSE**: Full support for MCP's streaming HTTP transport with Server-Sent Events + - **TCP Direct**: Raw TCP stream reconstruction for custom implementations + - **Chunked Transfer**: Handles HTTP chunked transfer encoding transparently +- **Protocol Compliance**: Validates JSON-RPC 2.0 structure and MCP-specific extensions + +### πŸš€ Advanced Capture Capabilities +- **Auto-Discovery Mode**: Intelligently detects MCP traffic on any port using pattern matching +- **TCP Stream Reassembly**: Reconstructs complete messages from fragmented packets +- **Multi-Stream Tracking**: Simultaneously monitors multiple MCP client-server connections +- **IPv4/IPv6 Dual Stack**: Native support for both IP protocols +- **Zero-Copy Architecture**: Efficient packet processing without client/server overhead + +### πŸ“Š Analysis & Visualization +- **Real-Time Web Dashboard**: Live traffic visualization with WebSocket updates +- **Message Flow Visualization**: Track request-response pairs using JSON-RPC IDs +- **Traffic Statistics**: Method frequency, error rates, response times +- **Search & Filter**: Query by method name, message type, content patterns +- **Export Capabilities**: Save captured sessions for offline analysis + +### πŸ› οΈ Developer Experience +- **MCP Server Integration**: Query captured data using MCP protocol itself + - FastMCP-based implementation for maximum compatibility + - Available tools: `query_traffic`, `search_traffic`, `get_stats`, `list_methods` + - Supports both stdio and HTTP transports +- **Multiple Interfaces**: + - Web UI for interactive exploration + - CLI for scripting and automation + - MCP server for programmatic access +- **Flexible Deployment**: + - Standalone sniffer mode + - Integrated web + sniffer + - Historical log analysis without active capture + +### MCP Transport Support + +| Official MCP Transport | Protocol Version | Capture Support | Details | +|------------------------|------------------|:---------------:|---------| +| **stdio** | All versions | coming soon :) | secret | +| **HTTP** (Streamable HTTP) | 2025-03-26+ | βœ… Full | HTTP POST with optional SSE streaming responses | +| **HTTP+SSE** (deprecated) | 2024-11-05 | βœ… Full | Legacy transport with separate SSE endpoint | + +Disclaimer: TCP direct traffic with JSON-RPC is also captured and marked as unknown (should you have custom stuff you shouldn't) ## Comparison with Similar Tools @@ -50,23 +79,20 @@ Non-exhaustive list: |-----------------------------------------------|:---------:|:------------:|:---------:| | Passive sniffing (no proxy needed) | βœ… | ❌ | βœ… | | MCP/JSON-RPC protocol awareness | βœ… | βœ… | ❌ | -| Auto-detect MCP traffic on any port | βœ… | ❌ | ❌ | +| SSE/Chunked HTTP support | βœ… | ❓ | ❌ | +| TCP stream reassembly | βœ… | ❌ | βœ… | +| Auto-detect MCP traffic | βœ… | ❌ | ❌ | | Web UI for live/historical traffic | βœ… | βœ… | ❌ | -| Can capture any traffic (not just via proxy) | βœ… | ❌ | βœ… | | JSON-RPC message type detection | βœ… | ❌ | ❌ | -| Message filtering by type | βœ… | ❌ | ❌ | -| Console-only mode (no web UI needed) | βœ… | ❌ | βœ… | -| Manual request crafting/testing | ❌ | βœ… | ❌ | -| Interactive tool/prompt testing | ❌ | βœ… | ❌ | -| Proxy/bridge between client/server | ❌ | βœ… | ❌ | -| No client/server config changes required | βœ… | ❌ | βœ… | -| General protocol analysis | ❌ | ❌ | βœ… | -| MCP-specific features | βœ… | βœ… | ❌ | +| MCP server for data access | βœ… | ❌ | ❌ | +| No client/server config needed | βœ… | ❌ | βœ… | +| Interactive testing/debugging | ❌ | βœ… | ❌ | +| Proxy/MITM capabilities | ❌ | βœ… | ❌ | **When to use each tool:** -- **MCPHawk**: Best for passively monitoring MCP traffic, debugging live connections, understanding protocol flow -- **mcpinspector**: Best for actively testing MCP servers, crafting custom requests, interactive debugging -- **Wireshark**: Best for general network analysis, non-MCP protocols, deep packet inspection +- **MCPHawk**: Passive monitoring, protocol analysis, debugging MCP implementations, understanding traffic patterns +- **mcpinspector**: Active testing, crafting requests, interactive debugging with proxy +- **Wireshark**: General network analysis, non-MCP protocols, packet-level inspection ## TLS/HTTPS Limitations @@ -81,11 +107,6 @@ MCPHawk captures **unencrypted** MCP traffic only. It cannot decrypt: - πŸ› **Troubleshooting local tools** - Monitor Claude Desktop, Cline, etc. with YOUR local MCP servers - πŸ“Š **Development/staging environments** - Where TLS is often disabled -**Not suitable for:** -- Production traffic analysis (usually encrypted) -- Cloud MCP services (HTTPS/WSS) -- Third-party MCP servers with TLS - ## Installation ### For Users @@ -141,8 +162,85 @@ sudo mcphawk web --port 3000 --host 0.0.0.0 --web-port 9000 # Enable debug output for troubleshooting sudo mcphawk sniff --port 3000 --debug sudo mcphawk web --port 3000 --debug + +# Start MCP server with Streamable HTTP transport (default) +mcphawk mcp --transport http --mcp-port 8765 + +# Start MCP server with stdio transport (for Claude Desktop integration) +mcphawk mcp --transport stdio + +# Start sniffer with integrated MCP server (HTTP transport) +sudo mcphawk sniff --port 3000 --with-mcp --mcp-transport http + +# Start web UI with integrated MCP server +sudo mcphawk web --port 3000 --with-mcp --mcp-transport http --mcp-port 8765 +``` + +## MCP Server Integration + +MCPHawk includes a built-in MCP server, allowing you to query captured traffic through the Model Context Protocol itself. This creates powerful possibilities: + +- **AI-Powered Analysis**: Connect Claude or other LLMs to analyze traffic patterns +- **Automated Monitoring**: Build agents that detect anomalies or specific behaviors +- **Integration Testing**: Programmatically verify MCP interactions in CI/CD pipelines + +MCPHawk Claude Desktop MCP + +### Available Tools + +The MCP server exposes these tools for traffic analysis: + +| Tool | Description | Parameters | +|------|-------------|------------| +| `query_traffic` | Fetch captured logs with pagination | `limit`, `offset` | +| `get_log` | Retrieve specific log entry | `log_id` | +| `search_traffic` | Search logs by content or type | `search_term`, `message_type`, `traffic_type`, `limit` | +| `get_stats` | Get traffic statistics | None | +| `list_methods` | List unique JSON-RPC methods | None | + +### Transport Options + +#### HTTP Transport (Development & Testing) + +The HTTP transport uses Server-Sent Events (SSE) for streaming responses: + +```bash +# Start MCP server +mcphawk mcp --transport http --mcp-port 8765 + +# Initialize session (note: returns SSE stream) +curl -N -X POST http://localhost:8765/mcp \ + -H 'Accept: text/event-stream' \ + -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' + +# Example response (SSE format): +# event: message +# data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05",...}} ``` +#### stdio Transport (Production & Claude Desktop) + +For Claude Desktop integration: + +```json +{ + "mcpServers": { + "mcphawk": { + "command": "mcphawk", + "args": ["mcp", "--transport", "stdio"] + } + } +} +``` + +The stdio transport follows the standard MCP communication pattern: +1. Client sends `initialize` request +2. Server responds with capabilities +3. Client sends `initialized` notification +4. Normal tool calls can proceed + +See [examples/mcp_sdk_client.py](examples/mcp_sdk_client.py) for HTTP client example or [examples/stdio_client.py](examples/stdio_client.py) for stdio communication. + ## Platform Support ### Tested Platforms @@ -154,7 +252,7 @@ sudo mcphawk web --port 3000 --debug - Requires elevated privileges (`sudo`) on macOS/Linux for packet capture - Limited to localhost/loopback interface monitoring -- WebSocket capture requires traffic to be uncompressed +- Cannot decrypt TLS/HTTPS traffic (WSS, HTTPS) - IPv6 support requires explicit interface configuration on some systems - High traffic volumes (>1000 msgs/sec) may impact performance @@ -170,18 +268,21 @@ sudo mcphawk web --auto-detect - Ensure the MCP server/client is using localhost (127.0.0.1 or ::1) - Check if traffic is on the expected port - Try auto-detect mode to find MCP traffic: `--auto-detect` -- On macOS, ensure you're allowing the terminal to capture packets in System Preferences +- Verify traffic is unencrypted (not HTTPS/TLS) +- On macOS, ensure Terminal has permission to capture packets in System Preferences -**WebSocket Traffic Not Showing:** -- Verify the WebSocket connection is uncompressed -- Check if the server is using IPv6 (::1) - MCPHawk supports both IPv4 and IPv6 -- Ensure the WebSocket frames contain valid JSON-RPC messages +**SSE/HTTP Responses Not Showing:** +- Confirm the server uses standard SSE format (event: message\ndata: {...}\n\n) +- Check if responses use chunked transfer encoding +- Enable debug mode to see detailed packet analysis: `--debug` ## Potential Upcoming Features Vote for features by opening a GitHub issue! - [x] **Auto-detect MCP traffic** - Automatically discover MCP traffic on any port without prior configuration +- [x] **MCP Server Interface** - Expose captured traffic via MCP server for AI agents to query and analyze traffic patterns +- [ ] **Stdio capture** - eBPF Integration (Linux/macOS) Trace read/write system calls for pipe communication - [ ] **Protocol Version Detection** - Identify and display MCP protocol version from captured traffic - [ ] **Smart Search & Filtering** - Search by method name, params, or any JSON field with regex support - [ ] **Performance Analytics** - Request/response timing, method frequency charts, and latency distribution @@ -192,7 +293,6 @@ Vote for features by opening a GitHub issue! - [ ] **Interactive Replay** - Click any request to re-send it, edit and replay captured messages - [ ] **Real-time Alerts** - Alert on specific methods or error patterns with webhook support - [ ] **Visualization** - Sequence diagrams, resource heat maps, method dependency graphs -- [ ] **MCP Server Interface** - Expose captured traffic via MCP server for AI agents to query and analyze traffic patterns ... and a few more off the deep end: - [ ] **TLS/HTTPS Support (MITM Proxy Mode)** - Optional man-in-the-middle proxy with certificate installation for encrypted traffic @@ -236,10 +336,3 @@ mcphawk web --port 3000 cd frontend && npm run build:watch # Auto-rebuild on changes mcphawk web --port 3000 # In another terminal ``` - -### Testing with Dummy Server - -```bash -# Generate various MCP patterns -python3 examples/generate_traffic/generate_all.py -``` diff --git a/examples/branding/mcphawk_claudedesktop.png b/examples/branding/mcphawk_claudedesktop.png new file mode 100644 index 0000000..bf47532 Binary files /dev/null and b/examples/branding/mcphawk_claudedesktop.png differ diff --git a/examples/branding/mcphawk_screenshot.png b/examples/branding/mcphawk_screenshot.png index 86d3a61..ca5ba1b 100644 Binary files a/examples/branding/mcphawk_screenshot.png and b/examples/branding/mcphawk_screenshot.png differ diff --git a/examples/generate_traffic/README.md b/examples/generate_traffic/README.md deleted file mode 100644 index 3c36f15..0000000 --- a/examples/generate_traffic/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# Traffic Generation Examples - -This directory contains example servers and clients for generating MCP traffic to test MCPHawk. - -## TCP-based MCP - -### Server -```bash -python3 tcp_server.py -``` -Starts a TCP MCP server on port 12345. - -### Client -```bash -python3 tcp_client.py -``` -Sends various MCP messages to the TCP server. - -## WebSocket-based MCP - -### Server -```bash -python3 ws_server.py -``` -Starts a WebSocket MCP server on port 8765. - -### Client -```bash -python3 ws_client.py -``` -Sends various MCP messages to the WebSocket server. - -## Generate All Traffic - -To generate both TCP and WebSocket traffic for testing: - -```bash -python3 generate_all.py -``` - -This will: -1. Start both TCP and WebSocket servers -2. Send a variety of MCP messages to each -3. Display the traffic being generated -4. Clean up when done - -## Testing with MCPHawk - -In another terminal, run MCPHawk to capture the traffic: - -```bash -# Capture TCP traffic -sudo mcphawk sniff --port 12345 - -# Capture WebSocket traffic -sudo mcphawk sniff --port 8765 - -# Capture both -sudo mcphawk sniff --filter "tcp port 12345 or tcp port 8765" - -# Or use auto-detect -sudo mcphawk sniff --auto-detect -``` \ No newline at end of file diff --git a/examples/generate_traffic/generate_all.py b/examples/generate_traffic/generate_all.py deleted file mode 100755 index c12dc0a..0000000 --- a/examples/generate_traffic/generate_all.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python3 -"""Generate both TCP and WebSocket MCP traffic for testing MCPHawk.""" - -import asyncio -import contextlib -import json -import logging -import socket -import subprocess -import sys -import time -from pathlib import Path - -import websockets - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s [%(levelname)s] %(message)s' -) -logger = logging.getLogger(__name__) - - -def run_tcp_server(): - """Run the TCP server in a subprocess.""" - script_path = Path(__file__).parent / "tcp_server.py" - return subprocess.Popen([sys.executable, str(script_path)]) - - -def run_ws_server(): - """Run the WebSocket server in a subprocess.""" - script_path = Path(__file__).parent / "ws_server.py" - return subprocess.Popen([sys.executable, str(script_path)]) - - -def send_tcp_traffic(): - """Send TCP MCP traffic.""" - logger.info("Sending TCP traffic...") - - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(('localhost', 12345)) - - messages = [ - {"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05"}, "id": 1}, - {"jsonrpc": "2.0", "method": "tools/list", "id": 2}, - {"jsonrpc": "2.0", "method": "notifications/tcp_test", "params": {"source": "tcp"}}, - {"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "test_tool", "arguments": {}}, "id": 3}, - ] - - for msg in messages: - data = json.dumps(msg).encode('utf-8') - sock.sendall(data) - logger.info(f" TCP sent: {msg.get('method')}") - - # Read response if it has an ID - if "id" in msg: - try: - response = sock.recv(1024) - if response: - logger.info(" TCP received response") - except Exception: - pass - - time.sleep(0.2) - - # Keep connection open a bit longer - time.sleep(1) - sock.close() - - except Exception as e: - logger.error(f"TCP client error: {e}") - - -async def send_ws_traffic(): - """Send WebSocket MCP traffic.""" - logger.info("Sending WebSocket traffic...") - - try: - async with websockets.connect("ws://localhost:8765", compression=None) as ws: - messages = [ - {"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "2024-11-05"}, "id": 1}, - {"jsonrpc": "2.0", "method": "tools/list", "id": 2}, - {"jsonrpc": "2.0", "method": "notifications/ws_test", "params": {"source": "websocket"}}, - {"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "calculator", "arguments": {"a": 10, "b": 20}}, "id": 3}, - ] - - for msg in messages: - await ws.send(json.dumps(msg)) - logger.info(f" WS sent: {msg.get('method')}") - - # Wait for response if it has an ID - if "id" in msg: - await ws.recv() - logger.info(" WS received response") - else: - await asyncio.sleep(0.2) - - except Exception as e: - logger.error(f"WebSocket client error: {e}") - - -def check_port(port): - """Check if a port is available.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = sock.connect_ex(('localhost', port)) - sock.close() - return result != 0 - - -async def main(): - """Generate all traffic types.""" - logger.info("MCPHawk Traffic Generator") - logger.info("========================\n") - - # Check if ports are available - if not check_port(12345): - logger.error("Port 12345 is already in use. Please stop the existing TCP server.") - return - - if not check_port(8765): - logger.error("Port 8765 is already in use. Please stop the existing WebSocket server.") - return - - logger.info("Starting servers...") - - # Start servers - tcp_server = run_tcp_server() - ws_server = run_ws_server() - - # Give servers time to start - logger.info("Waiting for servers to start...") - await asyncio.sleep(2) - - try: - # Send TCP traffic - send_tcp_traffic() - - # Send WebSocket traffic - await send_ws_traffic() - - logger.info("\nβœ… All traffic sent successfully!") - logger.info("\nServers will continue running. Press Ctrl+C to stop.") - - # Keep running - await asyncio.Future() - - except KeyboardInterrupt: - logger.info("\nStopping servers...") - finally: - # Clean up - tcp_server.terminate() - ws_server.terminate() - tcp_server.wait() - ws_server.wait() - logger.info("Servers stopped.") - - -if __name__ == "__main__": - with contextlib.suppress(KeyboardInterrupt): - asyncio.run(main()) - diff --git a/examples/generate_traffic/tcp_client.py b/examples/generate_traffic/tcp_client.py deleted file mode 100755 index 83d3785..0000000 --- a/examples/generate_traffic/tcp_client.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -"""TCP MCP client for testing.""" - -import json -import socket -import time - - -def send_mcp_message(sock, message): - """Send a JSON-RPC message.""" - data = json.dumps(message).encode('utf-8') - sock.sendall(data) - print(f"Sent: {message.get('method', message.get('result', 'response'))}") - - # Read response if message has an ID - if "id" in message: - try: - response = sock.recv(1024) - if response: - print(" Received response") - except Exception: - pass - - -def main(): - """Connect to TCP MCP server and send test messages.""" - print("Connecting to TCP MCP server on localhost:12345...") - - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.connect(('localhost', 12345)) - print("Connected!") - - # Send initialize - send_mcp_message(sock, { - "jsonrpc": "2.0", - "method": "initialize", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": {} - }, - "id": 1 - }) - time.sleep(0.5) - - # Send tools/list - send_mcp_message(sock, { - "jsonrpc": "2.0", - "method": "tools/list", - "id": 2 - }) - time.sleep(0.5) - - # Send a notification (no id) - send_mcp_message(sock, { - "jsonrpc": "2.0", - "method": "notifications/progress", - "params": { - "progress": 50, - "operation": "processing" - } - }) - time.sleep(0.5) - - # Send tools/call - send_mcp_message(sock, { - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "name": "calculator", - "arguments": {"a": 5, "b": 3} - }, - "id": 3 - }) - time.sleep(0.5) - - # Send a batch of messages quickly - print("\nSending batch of messages...") - for i in range(4, 8): - send_mcp_message(sock, { - "jsonrpc": "2.0", - "method": f"test/message_{i}", - "params": {"value": i * 10}, - "id": i - }) - time.sleep(0.1) - - print("\nAll messages sent!") - # Keep connection open briefly to avoid reset - time.sleep(2) - - except ConnectionRefusedError: - print("Error: Could not connect to server. Make sure tcp_server.py is running.") - except Exception as e: - print(f"Error: {e}") - - -if __name__ == "__main__": - main() - diff --git a/examples/generate_traffic/tcp_server.py b/examples/generate_traffic/tcp_server.py deleted file mode 100755 index 98cfcc4..0000000 --- a/examples/generate_traffic/tcp_server.py +++ /dev/null @@ -1,60 +0,0 @@ -import json -import socket -import threading - -HOST = "127.0.0.1" -PORT = 12345 # MCPHawk should sniff this port - - -def handle_client(conn, addr): - print(f"[DUMMY MCP] Connection from {addr}") - try: - while True: - data = conn.recv(1024) - if not data: - break - - raw_msg = data.decode(errors="ignore").strip() - print(f"[DUMMY MCP] Received: {raw_msg}") - - try: - # Parse incoming JSON-RPC request - request = json.loads(raw_msg) - request_id = request.get("id") - - # Build realistic JSON-RPC response - response = { - "jsonrpc": "2.0", - "result": "ok", - "id": request_id # echo back same id if present - } - - except json.JSONDecodeError: - print("[DUMMY MCP] Invalid JSON received, sending error response") - response = { - "jsonrpc": "2.0", - "error": {"code": -32700, "message": "Parse error"}, - "id": None - } - - # Send back response - conn.sendall((json.dumps(response) + "\n").encode()) - - finally: - conn.close() - - -def start_server(): - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind((HOST, PORT)) - server.listen() - print(f"[DUMMY MCP] Listening on {HOST}:{PORT}") - while True: - conn, addr = server.accept() - threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start() - - -if __name__ == "__main__": - start_server() - diff --git a/examples/generate_traffic/test_capture.py b/examples/generate_traffic/test_capture.py deleted file mode 100755 index 4a49c7a..0000000 --- a/examples/generate_traffic/test_capture.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -"""Test that MCPHawk can capture both TCP and WebSocket traffic.""" - -import json -import sqlite3 - - -def check_captured_messages(): - """Check the MCPHawk database for captured messages.""" - try: - conn = sqlite3.connect("mcphawk_logs.db") - cursor = conn.cursor() - - # Get total count - cursor.execute("SELECT COUNT(*) FROM logs") - total = cursor.fetchone()[0] - print(f"Total messages in database: {total}") - - # Check TCP messages (port 12345) - cursor.execute(""" - SELECT COUNT(*) FROM logs - WHERE (src_port = 12345 OR dst_port = 12345) - AND message LIKE '%jsonrpc%' - """) - tcp_count = cursor.fetchone()[0] - print(f"TCP MCP messages (port 12345): {tcp_count}") - - # Check WebSocket messages (port 8765) - cursor.execute(""" - SELECT COUNT(*) FROM logs - WHERE (src_port = 8765 OR dst_port = 8765) - AND message LIKE '%jsonrpc%' - """) - ws_count = cursor.fetchone()[0] - print(f"WebSocket MCP messages (port 8765): {ws_count}") - - # Show sample messages - if tcp_count > 0: - print("\nSample TCP messages:") - cursor.execute(""" - SELECT message FROM logs - WHERE (src_port = 12345 OR dst_port = 12345) - AND message LIKE '%jsonrpc%' - LIMIT 3 - """) - for row in cursor: - msg = json.loads(row[0]) - print(f" - {msg.get('method', msg.get('result', '?'))}") - - if ws_count > 0: - print("\nSample WebSocket messages:") - cursor.execute(""" - SELECT message FROM logs - WHERE (src_port = 8765 OR dst_port = 8765) - AND message LIKE '%jsonrpc%' - LIMIT 3 - """) - for row in cursor: - msg = json.loads(row[0]) - print(f" - {msg.get('method', msg.get('result', '?'))}") - - conn.close() - - # Summary - print("\n" + "="*50) - if tcp_count > 0 and ws_count > 0: - print("βœ… SUCCESS: Both TCP and WebSocket MCP traffic captured!") - elif tcp_count > 0: - print("⚠️ Only TCP traffic captured") - elif ws_count > 0: - print("⚠️ Only WebSocket traffic captured") - else: - print("❌ No MCP traffic captured") - - except Exception as e: - print(f"Error checking database: {e}") - - -if __name__ == "__main__": - print("MCPHawk Capture Test") - print("="*50) - print("\nChecking captured messages...") - print("(Make sure MCPHawk is running and traffic has been generated)\n") - - check_captured_messages() - diff --git a/examples/generate_traffic/ws_client.py b/examples/generate_traffic/ws_client.py deleted file mode 100755 index 8379800..0000000 --- a/examples/generate_traffic/ws_client.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -"""WebSocket MCP client for testing.""" - -import asyncio -import json -import logging - -import websockets - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -async def send_mcp_message(websocket, message): - """Send a JSON-RPC message and optionally wait for response.""" - await websocket.send(json.dumps(message)) - method = message.get('method', message.get('result', 'response')) - logger.info(f"Sent: {method}") - - # If it has an ID, wait for response - if "id" in message: - response = await websocket.recv() - response_data = json.loads(response) - logger.info(f"Received response: {response_data.get('result', response_data.get('error'))}") - return response_data - - -async def main(): - """Connect to WebSocket MCP server and send test messages.""" - uri = "ws://localhost:8765" - logger.info(f"Connecting to WebSocket MCP server at {uri}...") - - try: - async with websockets.connect(uri, compression=None) as websocket: - logger.info("Connected!") - - # Send initialize - await send_mcp_message(websocket, { - "jsonrpc": "2.0", - "method": "initialize", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": {} - }, - "id": 1 - }) - - # Send tools/list - await send_mcp_message(websocket, { - "jsonrpc": "2.0", - "method": "tools/list", - "id": 2 - }) - - # Send a notification (no id, no response expected) - await send_mcp_message(websocket, { - "jsonrpc": "2.0", - "method": "notifications/progress", - "params": { - "progress": 50, - "operation": "processing" - } - }) - await asyncio.sleep(0.5) # Small delay after notification - - # Send tools/call - await send_mcp_message(websocket, { - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "name": "calculator", - "arguments": {"a": 5, "b": 3} - }, - "id": 3 - }) - - # Send a large message to test extended length - large_data = "x" * 1000 - await send_mcp_message(websocket, { - "jsonrpc": "2.0", - "method": "test/large_message", - "params": {"data": large_data}, - "id": 4 - }) - - # Send a batch of messages quickly - logger.info("\nSending batch of messages...") - tasks = [] - for i in range(5, 10): - message = { - "jsonrpc": "2.0", - "method": f"test/message_{i}", - "params": {"value": i * 10}, - "id": i - } - tasks.append(send_mcp_message(websocket, message)) - - # Wait for all responses - await asyncio.gather(*tasks) - - # Send one more notification before closing - await send_mcp_message(websocket, { - "jsonrpc": "2.0", - "method": "notifications/closing", - "params": {"reason": "test complete"} - }) - - logger.info("\nAll messages sent!") - await asyncio.sleep(0.5) - - except websockets.exceptions.WebSocketException as e: - logger.error(f"WebSocket error: {e}") - except ConnectionRefusedError: - logger.error("Could not connect to server. Make sure ws_server.py is running.") - except Exception as e: - logger.error(f"Error: {e}") - - -if __name__ == "__main__": - asyncio.run(main()) - diff --git a/examples/generate_traffic/ws_server.py b/examples/generate_traffic/ws_server.py deleted file mode 100755 index 759dbea..0000000 --- a/examples/generate_traffic/ws_server.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -"""WebSocket MCP server for testing.""" - -import asyncio -import json -import logging - -import websockets - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -async def handle_mcp_message(websocket, message): - """Handle incoming MCP message and send response.""" - try: - data = json.loads(message) - method = data.get("method") - msg_id = data.get("id") - - logger.info(f"Received: {method} (id: {msg_id})") - - # Only respond to requests (with id), not notifications - if msg_id is None: - logger.info(f"Notification received: {method}") - return - - # Generate response based on method - if method == "initialize": - response = { - "jsonrpc": "2.0", - "result": { - "protocolVersion": "2024-11-05", - "capabilities": { - "tools": {"listChanged": True}, - "resources": {"subscribe": True} - }, - "serverInfo": { - "name": "test-ws-server", - "version": "1.0.0" - } - }, - "id": msg_id - } - elif method == "tools/list": - response = { - "jsonrpc": "2.0", - "result": { - "tools": [ - { - "name": "calculator", - "description": "Perform calculations", - "inputSchema": { - "type": "object", - "properties": { - "a": {"type": "number"}, - "b": {"type": "number"} - } - } - }, - { - "name": "echo", - "description": "Echo back input", - "inputSchema": { - "type": "object", - "properties": { - "message": {"type": "string"} - } - } - } - ] - }, - "id": msg_id - } - elif method == "tools/call": - params = data.get("params", {}) - tool_name = params.get("name") - args = params.get("arguments", {}) - - if tool_name == "calculator": - result = args.get("a", 0) + args.get("b", 0) - response = { - "jsonrpc": "2.0", - "result": {"value": result}, - "id": msg_id - } - else: - response = { - "jsonrpc": "2.0", - "result": {"echo": str(args)}, - "id": msg_id - } - else: - # Generic response - response = { - "jsonrpc": "2.0", - "result": {"status": "ok", "method": method}, - "id": msg_id - } - - await websocket.send(json.dumps(response)) - logger.info(f"Sent response for {method}") - - except json.JSONDecodeError: - logger.error(f"Invalid JSON: {message}") - except Exception as e: - logger.error(f"Error handling message: {e}") - if msg_id: - error_response = { - "jsonrpc": "2.0", - "error": { - "code": -32603, - "message": str(e) - }, - "id": msg_id - } - await websocket.send(json.dumps(error_response)) - - -async def mcp_server(websocket): - """Handle WebSocket connection.""" - logger.info("Client connected") - - try: - async for message in websocket: - await handle_mcp_message(websocket, message) - except websockets.exceptions.ConnectionClosed: - logger.info("Client disconnected") - except Exception as e: - logger.error(f"Connection error: {e}") - - -async def main(): - """Start WebSocket MCP server.""" - port = 8765 - logger.info(f"Starting WebSocket MCP server on ws://localhost:{port}") - - async with websockets.serve(mcp_server, "localhost", port, compression=None): - logger.info("Server ready. Press Ctrl+C to stop.") - await asyncio.Future() # Run forever - - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - logger.info("Server stopped.") - diff --git a/examples/http_sse_example.py b/examples/http_sse_example.py new file mode 100644 index 0000000..c484c62 --- /dev/null +++ b/examples/http_sse_example.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +Proper HTTP+SSE MCP client traffic generator. + +This simulates the legacy HTTP+SSE transport pattern as documented: +1. GET request to /sse endpoint to establish SSE connection +2. Server sends "endpoint" event with the message endpoint URL +3. Client POSTs JSON-RPC messages to the message endpoint + +This example creates traffic that MCPHawk can properly detect as HTTP+SSE. +""" + +import json +import threading +import time +from http.server import BaseHTTPRequestHandler, HTTPServer + +import requests + + +class MockHTTPSSEServer(BaseHTTPRequestHandler): + """Mock server that implements HTTP+SSE pattern.""" + + def log_message(self, format, *args): + """Suppress default logging.""" + pass + + def do_GET(self): + """Handle GET request for SSE connection.""" + if self.path == '/sse': + print("[Mock Server] Received GET /sse - sending SSE response") + self.send_response(200) + self.send_header('Content-Type', 'text/event-stream') + self.send_header('Cache-Control', 'no-cache') + self.send_header('Connection', 'keep-alive') + self.end_headers() + + # Send the endpoint event as per HTTP+SSE spec + endpoint_event = 'event: endpoint\ndata: {"url": "/messages"}\n\n' + self.wfile.write(endpoint_event.encode()) + self.wfile.flush() + + # For this example, close after sending endpoint + # Real servers would keep the connection open for streaming + return + else: + self.send_error(404) + + def do_POST(self): + """Handle POST request to message endpoint.""" + if self.path == '/messages': + print("[Mock Server] Received POST /messages") + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + # Parse the JSON-RPC request + try: + request = json.loads(post_data) + print(f"[Mock Server] Request: {request}") + + # Send a simple response + response = { + "jsonrpc": "2.0", + "result": {"initialized": True}, + "id": request.get("id") + } + + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(response).encode()) + except Exception as e: + print(f"[Mock Server] Error: {e}") + self.send_error(400) + else: + self.send_error(404) + + +def run_mock_server(port=8766): + """Run the mock HTTP+SSE server in a thread.""" + server = HTTPServer(('localhost', port), MockHTTPSSEServer) + server_thread = threading.Thread(target=server.serve_forever) + server_thread.daemon = True + server_thread.start() + return server + + +def simulate_http_sse_client(server_port=8766): + """Simulate HTTP+SSE client traffic pattern.""" + + print("\nSimulating HTTP+SSE MCP Client (Legacy Pattern)") + print("=" * 50) + + server_url = f"http://localhost:{server_port}" + + # Step 1: Establish SSE connection with GET request + print("\n1. Establishing SSE connection...") + print(f" GET {server_url}/sse") + print(" Accept: text/event-stream") + + # Use requests library for better control + session = requests.Session() + + endpoint_url = None + + try: + # Make GET request with SSE accept header + headers = {'Accept': 'text/event-stream'} + response = session.get(f"{server_url}/sse", headers=headers, stream=True, timeout=2) + + print(f" Response: {response.status_code} {response.reason}") + print(f" Content-Type: {response.headers.get('Content-Type')}") + + # Read the endpoint event with timeout on iter_lines + try: + for line in response.iter_lines(decode_unicode=True, chunk_size=1): + if line: + print(f" SSE: {line}") + if line.startswith('data:'): + data = json.loads(line[5:].strip()) + endpoint_url = data.get('url') + break + except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError): + print(" SSE connection closed/timed out (expected)") + + response.close() # Close the streaming connection + + except Exception as e: + print(f" Error during GET: {e}") + + # Always try the POST request, even if GET had issues + if not endpoint_url: + endpoint_url = "/messages" # Default endpoint for HTTP+SSE + print(f"\n2. Using default endpoint URL: {endpoint_url}") + else: + print(f"\n2. Server sent endpoint URL: {endpoint_url}") + + # Step 2: Send JSON-RPC request to the endpoint + print("\n3. Sending JSON-RPC request to endpoint...") + print(f" POST {server_url}{endpoint_url}") + print(" Content-Type: application/json") + + try: + # Send initialize request + initialize_request = { + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "http-sse-test", + "version": "1.0.0" + } + }, + "id": 1 + } + + # Important: No Accept header with dual types for HTTP+SSE POST + post_response = session.post( + f"{server_url}{endpoint_url}", + json=initialize_request, + headers={'Content-Type': 'application/json'}, + timeout=5 + ) + + print(f" Response: {post_response.status_code}") + if post_response.status_code == 200: + print(f" Result: {post_response.json()}") + + except Exception as e: + print(f" Error during POST: {e}") + + print("\n" + "=" * 50) + print("HTTP+SSE pattern demonstration complete") + print("Check MCPHawk to see the detected transport type:") + print("- GET /sse with Accept: text/event-stream β†’ HTTP+SSE") + print("- Server sends 'endpoint' event β†’ Confirms HTTP+SSE") + print("- POST to endpoint without dual Accept β†’ HTTP+SSE pattern") + + +if __name__ == "__main__": + print("HTTP+SSE MCP Client Example (Proper Implementation)") + print("This demonstrates the legacy HTTP+SSE transport pattern") + print("with a mock server that properly implements the protocol\n") + + # Start mock server + port = 8766 + print(f"Starting mock HTTP+SSE server on port {port}...") + server = run_mock_server(port) + time.sleep(1) # Give server time to start + + # Run client simulation + simulate_http_sse_client(port) + + # Keep server running briefly for any remaining packets + time.sleep(1) + print("\nDone!") diff --git a/examples/mcp_sdk_client.py b/examples/mcp_sdk_client.py new file mode 100755 index 0000000..20637fc --- /dev/null +++ b/examples/mcp_sdk_client.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Test MCPHawk server using the official MCP SDK client.""" + +import asyncio +import json + +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + + +async def test_with_sdk_client(): + """Test using the SDK's official client.""" + + # Connect to the server + async with streamablehttp_client("http://localhost:8765/mcp") as (read_stream, write_stream, session_id): + print(f"Connected with session ID: {session_id}") + + # Create a session + async with ClientSession(read_stream, write_stream) as session: + # Initialize + print("\n1. Initializing...") + await session.initialize() + print("Initialized successfully") + + # List tools + print("\n2. Listing tools...") + tools_result = await session.list_tools() + print(f"Available tools: {len(tools_result.tools)}") + for tool in tools_result.tools: + print(f" - {tool.name}: {tool.description}") + + # Call a tool + print("\n3. Calling get_stats...") + result = await session.call_tool("get_stats", arguments={}) + print("Result:") + for content in result.content: + print(f" {content.text}") + + # Try query_traffic + print("\n4. Querying recent traffic...") + result = await session.call_tool("query_traffic", arguments={"limit": 5}) + print("Result:") + for content in result.content: + data = json.loads(content.text) + print(f" Found {len(data)} log entries") + + +if __name__ == "__main__": + print("Testing MCPHawk MCP Server with SDK Client") + print("==========================================") + print("Make sure the server is running with:") + print(" mcphawk mcp --transport http --mcp-port 8765") + print() + + try: + asyncio.run(test_with_sdk_client()) + except Exception as e: + print(f"Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + diff --git a/examples/stdio_client.py b/examples/stdio_client.py new file mode 100644 index 0000000..b483088 --- /dev/null +++ b/examples/stdio_client.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +""" +Example stdio client for MCPHawk MCP server. + +This demonstrates how to communicate with MCPHawk's MCP server using the stdio transport. +The MCP protocol requires: +1. Initialize request +2. Initialized notification +3. Then you can make tool calls +""" + +import json +import queue +import subprocess +import threading +from typing import Any, Optional + + +class MCPHawkStdioClient: + """Client for communicating with MCPHawk MCP server over stdio.""" + + def __init__(self, debug: bool = False): + self.debug = debug + self.proc = None + self.stderr_queue = queue.Queue() + self.request_id = 0 + + def connect(self) -> bool: + """Start the MCP server process and initialize connection.""" + try: + # Start the MCP server + self.proc = subprocess.Popen( + ["mcphawk", "mcp", "--transport", "stdio"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=0 # Unbuffered + ) + + # Start stderr reader thread + self.stderr_thread = threading.Thread(target=self._read_stderr, daemon=True) + self.stderr_thread.start() + + # Send initialize request + init_response = self._send_request({ + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "mcphawk-stdio-client", "version": "1.0"} + } + }) + + if not init_response or "error" in init_response: + print(f"Failed to initialize: {init_response}") + return False + + # Send initialized notification + self._send_notification({ + "method": "notifications/initialized", + "params": {} + }) + + if self.debug: + print(f"Connected to server: {init_response['result']['serverInfo']}") + + return True + + except Exception as e: + print(f"Failed to connect: {e}") + return False + + def _read_stderr(self): + """Read stderr in a separate thread.""" + while self.proc and self.proc.poll() is None: + line = self.proc.stderr.readline() + if line: + self.stderr_queue.put(line.strip()) + + def _send_request(self, request: dict[str, Any]) -> Optional[dict[str, Any]]: + """Send a JSON-RPC request and wait for response.""" + self.request_id += 1 + request["jsonrpc"] = "2.0" + request["id"] = self.request_id + + request_str = json.dumps(request) + if self.debug: + print(f">>> {request_str}") + + self.proc.stdin.write(request_str + "\n") + self.proc.stdin.flush() + + # Read response + response_line = self.proc.stdout.readline() + if response_line: + try: + response = json.loads(response_line) + if self.debug: + print(f"<<< {json.dumps(response, indent=2)}") + return response + except json.JSONDecodeError as e: + print(f"Failed to decode response: {e}") + print(f"Raw: {response_line}") + return None + return None + + def _send_notification(self, notification: dict[str, Any]) -> None: + """Send a JSON-RPC notification (no response expected).""" + notification["jsonrpc"] = "2.0" + + notification_str = json.dumps(notification) + if self.debug: + print(f">>> {notification_str}") + + self.proc.stdin.write(notification_str + "\n") + self.proc.stdin.flush() + + def list_tools(self) -> Optional[list]: + """Get list of available tools.""" + response = self._send_request({ + "method": "tools/list", + "params": {} + }) + + if response and "result" in response: + return response["result"]["tools"] + return None + + def call_tool(self, tool_name: str, arguments: Optional[dict[str, Any]] = None) -> Optional[Any]: + """Call a tool with given arguments.""" + response = self._send_request({ + "method": "tools/call", + "params": { + "name": tool_name, + "arguments": arguments or {} + } + }) + + if response and "result" in response: + # Extract the text content from the response + content = response["result"]["content"] + if content and len(content) > 0: + text = content[0]["text"] + try: + # Try to parse as JSON + return json.loads(text) + except json.JSONDecodeError: + # Return as plain text if not JSON + return text + return None + + def close(self): + """Close the connection.""" + if self.proc: + self.proc.terminate() + self.proc.wait() + self.proc = None + + +def main(): + """Example usage of the MCPHawk stdio client.""" + client = MCPHawkStdioClient(debug=True) + + print("Connecting to MCPHawk MCP server...") + if not client.connect(): + print("Failed to connect!") + return + + print("\n1. Listing available tools:") + tools = client.list_tools() + if tools: + for tool in tools: + print(f" - {tool['name']}: {tool['description']}") + + print("\n2. Getting traffic statistics:") + stats = client.call_tool("get_stats") + if stats: + print(f" Total logs: {stats['total']}") + print(f" Requests: {stats['requests']}") + print(f" Responses: {stats['responses']}") + print(f" Notifications: {stats['notifications']}") + print(f" Errors: {stats['errors']}") + + print("\n3. Querying recent traffic:") + logs = client.call_tool("query_traffic", {"limit": 5}) + if logs: + print(f" Found {len(logs)} recent log entries") + for log in logs: + msg_preview = log['message'][:50] + "..." if len(log['message']) > 50 else log['message'] + print(f" - {log['timestamp']}: {msg_preview}") + + print("\n4. Listing captured methods:") + methods = client.call_tool("list_methods") + if methods: + print(f" Found {len(methods)} unique methods:") + for method in methods[:10]: # Show first 10 + print(f" - {method}") + if len(methods) > 10: + print(f" ... and {len(methods) - 10} more") + + print("\nClosing connection...") + client.close() + + +if __name__ == "__main__": + main() diff --git a/examples/streamable_http_example.py b/examples/streamable_http_example.py new file mode 100644 index 0000000..aaeaa66 --- /dev/null +++ b/examples/streamable_http_example.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +Example Streamable HTTP MCP client traffic generator. + +This demonstrates the Streamable HTTP transport pattern for testing MCPHawk's transport detection. +Streamable HTTP uses: +1. POST request with dual Accept headers (application/json, text/event-stream) +2. Server can respond with either JSON or SSE +""" + +import json +import urllib.error +import urllib.request + + +def simulate_streamable_http_client(): + """Simulate Streamable HTTP client traffic pattern.""" + + print("Simulating Streamable HTTP MCP Client") + print("=" * 50) + + server_url = "http://localhost:8765" + + print("\n1. Sending request with dual Accept headers (Streamable HTTP pattern)...") + print(f" POST {server_url}/mcp") + print(" Accept: application/json, text/event-stream") + print(" Content-Type: application/json") + + # Send a sample initialize request + initialize_request = { + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", # New protocol version + "capabilities": {}, + "clientInfo": { + "name": "streamable-http-test", + "version": "1.0.0" + } + }, + "id": 1 + } + + try: + data = json.dumps(initialize_request).encode('utf-8') + req = urllib.request.Request( + f"{server_url}/mcp", + data=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream" # Key difference! + } + ) + + print(f"\n Request body: {json.dumps(initialize_request, indent=2)}") + + with urllib.request.urlopen(req) as response: + print(f"\n Response: {response.status}") + content_type = response.headers.get('Content-Type', '') + print(f" Content-Type: {content_type}") + + if 'text/event-stream' in content_type: + print(" Server returned SSE response (streaming)") + else: + print(" Server returned JSON response") + + except urllib.error.URLError as e: + print(f" Connection failed: {e}") + + # Send another request that might get a different response type + print("\n2. Sending tool call request...") + print(f" POST {server_url}/mcp") + print(" Accept: application/json, text/event-stream") + + tool_request = { + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "long_running_tool", + "arguments": {} + }, + "id": 2 + } + + try: + data = json.dumps(tool_request).encode('utf-8') + req = urllib.request.Request( + f"{server_url}/mcp", + data=data, + headers={ + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream" + } + ) + + with urllib.request.urlopen(req) as response: + print(f"\n Response: {response.status}") + content_type = response.headers.get('Content-Type', '') + print(f" Content-Type: {content_type}") + + except urllib.error.URLError as e: + print(f" Connection failed: {e}") + + print("\n" + "=" * 50) + print("Streamable HTTP pattern demonstration complete") + print("Check MCPHawk to see how it detected the transport type:") + print("- POST with Accept: application/json, text/event-stream β†’ Streamable HTTP") + + +if __name__ == "__main__": + print("Streamable HTTP MCP Client Example") + print("This demonstrates the Streamable HTTP transport pattern") + print("This should work with our MCP server\n") + + simulate_streamable_http_client() diff --git a/examples/test_mcp_http.sh b/examples/test_mcp_http.sh new file mode 100755 index 0000000..d3f58f5 --- /dev/null +++ b/examples/test_mcp_http.sh @@ -0,0 +1,177 @@ +#!/bin/bash + +# Test MCPHawk MCP Server with various requests +# Make sure MCPHawk is running with: +# sudo mcphawk web --auto-detect --with-mcp --mcp-transport http --mcp-port 8765 --debug + +SESSION_ID="test-session-$(date +%s)" +MCP_URL="http://localhost:8765/mcp" + +echo "Testing MCPHawk MCP Server with session: $SESSION_ID" +echo "================================================" + +# 1. Initialize session +echo -e "\n1. Initializing MCP session..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0" + } + }, + "id": 1 + }' | jq . + +sleep 1 + +# 2. Send initialized notification +echo -e "\n2. Sending initialized notification..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "notifications/initialized", + "params": {} + }' | jq . + +sleep 1 + +# 3. List available tools +echo -e "\n3. Listing available tools..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/list", + "params": {}, + "id": 2 + }' | jq . + +sleep 1 + +# 4. Get traffic statistics +echo -e "\n4. Getting traffic statistics..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_stats", + "arguments": {} + }, + "id": 3 + }' | jq . + +sleep 1 + +# 5. Query recent traffic +echo -e "\n5. Querying recent traffic (limit 10)..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "query_traffic", + "arguments": { + "limit": 10 + } + }, + "id": 4 + }' | jq . + +sleep 1 + +# 6. List unique methods captured +echo -e "\n6. Listing unique JSON-RPC methods..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "list_methods", + "arguments": {} + }, + "id": 5 + }' | jq . + +sleep 1 + +# 7. Search for specific traffic +echo -e "\n7. Searching for 'initialize' in traffic..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "search_traffic", + "arguments": { + "search_term": "initialize" + } + }, + "id": 6 + }' | jq . + +sleep 1 + +# 8. Test error handling - call non-existent tool +echo -e "\n8. Testing error handling (calling non-existent tool)..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "non_existent_tool", + "arguments": {} + }, + "id": 7 + }' | jq . + +sleep 1 + +# 9. Send a notification (no ID, no response expected) +echo -e "\n9. Sending a notification..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "notifications/progress", + "params": { + "progress": 50, + "message": "Test progress notification" + } + }' + +echo -e "\n\nAll tests completed!" +echo "Check the MCPHawk web UI at http://localhost:8000 to see:" +echo "- All requests marked with purple 'MCP' badges" +echo "- Use the MCPHawk toggle button to filter these messages" +echo "- Click on messages to see full JSON details" \ No newline at end of file diff --git a/examples/test_mcp_sdk.sh b/examples/test_mcp_sdk.sh new file mode 100755 index 0000000..e5dfc38 --- /dev/null +++ b/examples/test_mcp_sdk.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Test MCPHawk MCP Server (SDK version) with proper session flow +# The SDK requires: +# 1. First initialize request WITHOUT session ID +# 2. Server returns session ID in response +# 3. Use that session ID for subsequent requests + +MCP_URL="http://localhost:8765/mcp" + +echo "Testing MCPHawk MCP Server (SDK version)" +echo "========================================" + +# 1. Initialize WITHOUT session ID - server will assign one +echo -e "\n1. Initializing MCP session (no session ID)..." +response=$(curl -s -i -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -d '{ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0" + } + }, + "id": 1 + }') + +echo "$response" + +# Extract session ID from response headers +SESSION_ID=$(echo "$response" | grep -i "mcp-session-id:" | sed 's/.*: //' | tr -d '\r\n') + +if [ -z "$SESSION_ID" ]; then + echo "ERROR: No session ID received from server" + exit 1 +fi + +echo -e "\nReceived session ID: $SESSION_ID" + +sleep 1 + +# 2. Now use the server-provided session ID for subsequent requests +echo -e "\n2. Listing tools with session ID..." +curl -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/list", + "params": {}, + "id": 2 + }' | jq . + +sleep 1 + +# 3. Test a notification (should return no response) +echo -e "\n3. Sending notification..." +curl -i -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "notifications/progress", + "params": { + "progress": 50, + "message": "Test progress notification" + } + }' + +echo -e "\n\nCheck http://localhost:8000 for captured traffic" \ No newline at end of file diff --git a/examples/test_mcp_sdk_sse.sh b/examples/test_mcp_sdk_sse.sh new file mode 100755 index 0000000..8be5813 --- /dev/null +++ b/examples/test_mcp_sdk_sse.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# Test MCPHawk MCP Server (SDK version) with SSE response handling + +MCP_URL="http://localhost:8765/mcp" + +echo "Testing MCPHawk MCP Server (SDK version) with SSE" +echo "=================================================" + +# 1. Initialize WITHOUT session ID +echo -e "\n1. Initializing MCP session..." +response=$(curl -s -i -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -d '{ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0" + } + }, + "id": 1 + }') + +# Extract session ID +SESSION_ID=$(echo "$response" | grep -i "mcp-session-id:" | sed 's/.*: //' | tr -d '\r\n') +echo "Session ID: $SESSION_ID" + +# Extract JSON from SSE response +json_data=$(echo "$response" | grep "^data: " | sed 's/^data: //') +echo "Response JSON:" +echo "$json_data" | jq . + +sleep 1 + +# 2. List tools +echo -e "\n2. Listing tools..." +response=$(curl -s -i -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/list", + "params": {}, + "id": 2 + }') + +# Extract JSON from SSE +json_data=$(echo "$response" | grep "^data: " | sed 's/^data: //') +echo "$json_data" | jq . + +sleep 1 + +# 3. Call a tool +echo -e "\n3. Getting stats..." +response=$(curl -s -i -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_stats", + "arguments": {} + }, + "id": 3 + }') + +json_data=$(echo "$response" | grep "^data: " | sed 's/^data: //') +echo "$json_data" | jq . + +# 4. Test standard MCP notification (not custom) +echo -e "\n4. Sending standard initialized notification..." +curl -s -i -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "notifications/initialized", + "params": {} + }' | head -10 + +echo -e "\n\nNote: Responses use Server-Sent Events (SSE) format" +echo "The sniffer might not capture SSE responses properly" \ No newline at end of file diff --git a/examples/test_mcp_sdk_wait.sh b/examples/test_mcp_sdk_wait.sh new file mode 100755 index 0000000..ebeea64 --- /dev/null +++ b/examples/test_mcp_sdk_wait.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# Test MCPHawk MCP Server with proper initialization wait + +MCP_URL="http://localhost:8765/mcp" + +echo "Testing MCPHawk MCP Server (SDK version)" +echo "========================================" + +# 1. Initialize +echo -e "\n1. Initializing MCP session..." +response=$(curl -s -i -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -d '{ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0" + } + }, + "id": 1 + }') + +SESSION_ID=$(echo "$response" | grep -i "mcp-session-id:" | sed 's/.*: //' | tr -d '\r\n') +echo "Session ID: $SESSION_ID" + +# 2. Send initialized notification (this might be required by SDK) +echo -e "\n2. Sending initialized notification to complete handshake..." +curl -s -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "notifications/initialized", + "params": {} + }' + +echo "Waiting for initialization to complete..." +sleep 2 + +# 3. Now try listing tools +echo -e "\n3. Listing tools..." +response=$(curl -s -i -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/list", + "params": {}, + "id": 2 + }') + +json_data=$(echo "$response" | grep "^data: " | sed 's/^data: //') +if [ -n "$json_data" ]; then + echo "$json_data" | jq . +else + echo "Raw response:" + echo "$response" | tail -20 +fi + +# 4. Test calling a tool +echo -e "\n4. Calling get_stats tool..." +response=$(curl -s -X POST $MCP_URL \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "get_stats", + "arguments": {} + }, + "id": 3 + }') + +# Try to extract JSON - SDK might return plain JSON for errors +echo "$response" | jq . 2>/dev/null || echo "$response" \ No newline at end of file diff --git a/examples/test_mcp_simple.sh b/examples/test_mcp_simple.sh new file mode 100755 index 0000000..cb8c054 --- /dev/null +++ b/examples/test_mcp_simple.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Simple test for MCPHawk MCP Server +# Run MCPHawk with: sudo mcphawk web --auto-detect --with-mcp --mcp-transport http --mcp-port 8765 --debug + +SESSION_ID="test-$(date +%s)" +echo "Testing with session: $SESSION_ID" + +# Single test request +echo "Sending initialize request..." +curl -v -X POST http://localhost:8765/mcp \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -H "mcp-session-id: $SESSION_ID" \ + -d '{ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0" + } + }, + "id": 1 + }' 2>&1 + +echo -e "\n\nCheck http://localhost:8000 for captured traffic" \ No newline at end of file diff --git a/frontend/src/components/LogTable/LogFilters.vue b/frontend/src/components/LogTable/LogFilters.vue index 8bc7ac6..00e34e3 100644 --- a/frontend/src/components/LogTable/LogFilters.vue +++ b/frontend/src/components/LogTable/LogFilters.vue @@ -48,6 +48,21 @@ + + +