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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
file: ./coverage.xml
files: ./coverage.xml
flags: unittests
name: codecov-${{ matrix.python-version }}
token: ${{ secrets.CODECOV_TOKEN }}
token: ${{ secrets.CODECOV_TOKEN }}
override_commit: ${{ github.event.pull_request.head.sha }}
override_pr: ${{ github.event.number }}
override_branch: ${{ github.head_ref }}
verbose: true
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
</div>

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.
MCPHawk is a new Logging & Monitoring solution for **Model Context Protocol (MCP)** traffic, providing deep visibility into MCP client-server interactions. It started off as a mix between Wireshark and mcpinspector, purpose-built for the MCP ecosystem, and is now slowly turning into something more.

**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
- **Protocol-Aware Capture**: Understands MCP's JSON-RPC 2.0 transport layer, capturing and reassembling messages from stdio pipes and HTTP streams
- **Transport Agnostic**: Monitors MCP traffic across all standard transports (stdio, HTTP Streaming, HTTP+SSE)
- **Full Message Reconstruction**: Advanced stream reassembly handles fragmented packets, chunked HTTP transfers, SSE streams, and stdio pipes

<img src="examples/branding/mcphawk_screenshot.png" alt="MCPHawk Screenshot" width="100%">

Expand All @@ -29,9 +28,7 @@ MCPHawk is a passive network analyzer for **Model Context Protocol (MCP)** traff
- **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
- **Transport-Specific Handling**: See MCP Transport Support table below for full details
- **Chunked Transfer**: Handles HTTP chunked transfer encoding transparently
- **Protocol Compliance**: Validates JSON-RPC 2.0 structure and MCP-specific extensions

Expand Down Expand Up @@ -67,11 +64,11 @@ MCPHawk is a passive network analyzer for **Model Context Protocol (MCP)** traff

| 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 |
| **stdio** | All versions | ✅ Full | Process wrapper transparently captures stdin/stdout between client and server |
| **HTTP Streaming** | 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)
Note: Raw TCP traffic with JSON-RPC is also captured and marked as "unknown" transport type

## Comparison with Similar Tools

Expand All @@ -87,7 +84,7 @@ Disclaimer: TCP direct traffic with JSON-RPC is also captured and marked as unkn
| MCP server for data access | ✅ | ❌ | ❌ |
| No client/server config needed | ✅ | ❌ | ✅ |
| Interactive testing/debugging | ❌ | ✅ | ❌ |
| Proxy/MITM capabilities | | ✅ | ❌ |
| Proxy/MITM capabilities | ✅ (stdio) | ✅ | ❌ |

**When to use each tool:**
- **MCPHawk**: Passive monitoring, protocol analysis, debugging MCP implementations, understanding traffic patterns
Expand Down Expand Up @@ -163,6 +160,22 @@ sudo mcphawk web --port 3000 --host 0.0.0.0 --web-port 9000
sudo mcphawk sniff --port 3000 --debug
sudo mcphawk web --port 3000 --debug

# Wrap an MCP server to capture stdio traffic
mcphawk wrap /path/to/mcp-server --arg1 --arg2

# Example: Wrap Context7 MCP server to monitor Claude Desktop's documentation lookups
mcphawk wrap npx -y @upstash/context7-mcp@latest

# Claude Desktop config to use the wrapped version:
# {
# "mcpServers": {
# "context7": {
# "command": "mcphawk",
# "args": ["wrap", "npx", "-y", "@upstash/context7-mcp@latest"]
# }
# }
# }

# Start MCP server with Streamable HTTP transport (default)
mcphawk mcp --transport http --mcp-port 8765

Expand Down Expand Up @@ -282,7 +295,7 @@ 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
- [x] **Stdio capture** - Transparent process wrapper to capture stdin/stdout 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
Expand Down
10 changes: 7 additions & 3 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Minimal codecov configuration
coverage:
status:
project:
default:
target: 80 # Automatically set coverage target
threshold: 5% # Allow 5% drop in coverage
target: 80
threshold: 5

comment:
layout: "reach, diff, flags, files"
behavior: default
require_changes: false
Binary file modified examples/branding/mcphawk_claudedesktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/branding/mcphawk_screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 29 additions & 15 deletions frontend/src/components/LogTable/LogFilters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@
<MagnifyingGlassIcon class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
</div>

<!-- Transport type filter -->
<select
v-model="selectedTransport"
@change="logStore.setTransportFilter(selectedTransport)"
class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-mcp-blue focus:border-transparent"
>
<option value="all">All Transports</option>
<option value="streamable_http">Streamable HTTP</option>
<option value="http_sse">HTTP+SSE</option>
<option value="stdio">stdio</option>
<option value="unknown">Unknown</option>
</select>

<!-- Server filter -->
<select
v-if="logStore.uniqueServers.length > 0"
v-model="selectedServer"
@change="logStore.setServerFilter(selectedServer)"
class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-mcp-blue focus:border-transparent"
>
<option value="all">All Servers</option>
<option v-for="server in logStore.uniqueServers" :key="server" :value="server">
{{ server }}
</option>
</select>

<!-- Expand all -->
<button
@click="logStore.toggleExpandAll"
Expand All @@ -48,20 +74,6 @@
<span class="hidden sm:inline">{{ logStore.expandAll ? 'Collapse' : 'Expand' }}</span>
</button>

<!-- Toggle MCPHawk traffic -->
<button
@click="logStore.toggleMcpHawkTraffic"
class="px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2"
:class="[
logStore.showMcpHawkTraffic
? 'bg-purple-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
]"
:title="logStore.showMcpHawkTraffic ? 'Hide MCPHawk\'s own MCP traffic' : 'Show MCPHawk\'s own MCP traffic'"
>
<FunnelIcon class="h-5 w-5" />
<span class="hidden lg:inline">MCPHawk</span>
</button>

<!-- Clear logs -->
<button
Expand All @@ -87,11 +99,13 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { useLogStore } from '@/stores/logs'
import { MagnifyingGlassIcon, TrashIcon, ArrowPathIcon, CodeBracketIcon, FunnelIcon } from '@heroicons/vue/24/outline'
import { MagnifyingGlassIcon, TrashIcon, ArrowPathIcon, CodeBracketIcon } from '@heroicons/vue/24/outline'

const logStore = useLogStore()

const searchQuery = ref('')
const selectedTransport = ref('all')
const selectedServer = ref('all')

const filters = computed(() => [
{ label: 'All', value: 'all', count: logStore.stats.total },
Expand Down
71 changes: 52 additions & 19 deletions frontend/src/components/LogTable/LogRow.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<tr>
<td colspan="7" class="p-0">
<td colspan="10" class="p-0">
<div
class="cursor-pointer transition-all relative"
:class="{
Expand All @@ -17,19 +17,23 @@
<td class="px-4 py-3 text-left w-32 text-sm text-gray-900 dark:text-gray-100">
{{ formatTimestamp(log.timestamp) }}
</td>
<td class="px-4 py-3 text-left w-40">
<div class="flex items-center gap-2 whitespace-nowrap">
<MessageTypeBadge :type="messageType" />
<span v-if="isMcpHawkTraffic"
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
title="MCPHawk's own MCP traffic">
MCP🦅
</span>
</div>
<td class="px-4 py-3 text-left w-32">
<MessageTypeBadge :type="messageType" />
</td>
<td class="px-4 py-3 text-left w-20 text-sm text-gray-900 dark:text-gray-100 font-mono">
<span class="text-gray-600 dark:text-gray-400">{{ messageId }}</span>
</td>
<td class="px-4 py-3 text-left text-sm text-gray-900 dark:text-gray-100 font-mono truncate">
{{ messageSummary }}
</td>
<td class="px-4 py-3 text-left w-40 text-sm text-gray-500 dark:text-gray-400">
<span v-if="serverInfo"
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200"
:title="`${serverInfo.name} v${serverInfo.version}`">
{{ serverInfo.name }}
</span>
<span v-else class="text-gray-400 dark:text-gray-600">-</span>
</td>
<td class="px-4 py-3 text-left w-32 text-sm text-gray-500 dark:text-gray-400 font-mono">
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
Expand All @@ -48,6 +52,9 @@
<td class="px-4 py-3 text-left w-24 text-sm text-gray-500 dark:text-gray-400 font-mono">
{{ portInfo }}
</td>
<td class="px-4 py-3 text-left w-20 text-sm text-gray-500 dark:text-gray-400 font-mono">
{{ pidInfo }}
</td>
</tr>
</table>
</div>
Expand Down Expand Up @@ -99,24 +106,26 @@ defineEmits(['click'])
const messageType = computed(() => getMessageType(props.log.message))
const messageSummary = computed(() => getMessageSummary(props.log.message))
const portInfo = computed(() => getPortInfo(props.log))
const directionIcon = computed(() => getDirectionIcon(props.log.direction))

const formattedJson = computed(() => {
const messageId = computed(() => {
try {
const parsed = JSON.parse(props.log.message)
return JSON.stringify(parsed, null, 2)
if (parsed && parsed.id !== undefined) {
return parsed.id
}
} catch {
return props.log.message
// ignore
}
return '-'
})
const directionIcon = computed(() => getDirectionIcon(props.log.direction))

const isMcpHawkTraffic = computed(() => {
if (!props.log.metadata) return false
const formattedJson = computed(() => {
try {
const meta = JSON.parse(props.log.metadata)
return meta.source === 'mcphawk-mcp'
const parsed = JSON.parse(props.log.message)
return JSON.stringify(parsed, null, 2)
} catch {
return false
return props.log.message
}
})

Expand All @@ -128,4 +137,28 @@ const transportTypeColor = computed(() => {
return getTransportTypeColor(props.log.transport_type || props.log.traffic_type || 'unknown')
})

const pidInfo = computed(() => {
// For stdio transport, show PID; for network transports, show empty
if (props.log.transport_type === 'stdio' && props.log.pid) {
return props.log.pid.toString()
}
return '-'
})

const serverInfo = computed(() => {
if (!props.log.metadata) return null
try {
const meta = JSON.parse(props.log.metadata)
if (meta.server_name) {
return {
name: meta.server_name,
version: meta.server_version || ''
}
}
} catch {
// ignore
}
return null
})

</script>
13 changes: 11 additions & 2 deletions frontend/src/components/LogTable/LogTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-32">
Time
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-40">
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-32">
Type
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-20">
ID
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Message
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-40">
Server
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-32">
Transport
</th>
Expand All @@ -31,6 +37,9 @@
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-24">
Port
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-20">
PID
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
Expand All @@ -46,7 +55,7 @@

<!-- Empty state -->
<tr v-if="!logStore.loading && displayLogs.length === 0">
<td colspan="7" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<td colspan="10" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center">
<svg class="w-12 h-12 mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
Expand Down
Loading