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
94 changes: 93 additions & 1 deletion frontend/src/components/LogTable/LogRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,84 @@
</div>
<!-- Expanded JSON view -->
<div v-if="isExpanded" class="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
<!-- Message metadata -->
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span class="text-gray-500 dark:text-gray-400">Date:</span>
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
{{ formatDate(log.timestamp) }}
</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Time:</span>
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
{{ formatTimestamp(log.timestamp) }}
</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Type:</span>
<MessageTypeBadge :type="messageType" class="ml-2" />
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Message ID:</span>
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
{{ messageId }}
</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Server:</span>
<span v-if="serverInfo" class="ml-2 text-gray-900 dark:text-gray-100">
{{ serverInfo.name }} <span class="text-gray-500 dark:text-gray-400">v{{ serverInfo.version }}</span>
</span>
<span v-else class="ml-2 text-gray-500 dark:text-gray-400">-</span>
</div>
<div v-if="clientInfo">
<span class="text-gray-500 dark:text-gray-400">Client:</span>
<span class="ml-2 text-gray-900 dark:text-gray-100">
{{ clientInfo.name }} <span class="text-gray-500 dark:text-gray-400">v{{ clientInfo.version }}</span>
</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Transport:</span>
<span
class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
:class="transportTypeColor"
>
{{ formattedTransportType }}
</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Direction:</span>
<span class="ml-2 text-gray-900 dark:text-gray-100">
{{ log.src_ip }} {{ directionIcon }} {{ log.dst_ip }}
</span>
</div>
<div v-if="log.src_port || log.dst_port">
<span class="text-gray-500 dark:text-gray-400">Ports:</span>
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
{{ portInfo }}
</span>
</div>
<div v-if="log.pid">
<span class="text-gray-500 dark:text-gray-400">PID:</span>
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
{{ log.pid }}
</span>
</div>
<div v-if="log.log_id">
<span class="text-gray-500 dark:text-gray-400">Log ID:</span>
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100 text-xs">
{{ log.log_id }}
</span>
</div>
</div>
</div>

<!-- JSON content -->
<div class="px-4 py-3">
<pre class="text-xs font-mono text-gray-800 dark:text-gray-200 overflow-x-auto">{{ formattedJson }}</pre>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">JSON-RPC Message:</div>
<pre class="text-xs font-mono text-gray-800 dark:text-gray-200 overflow-x-auto bg-gray-100 dark:bg-gray-800 p-3 rounded">{{ formattedJson }}</pre>
</div>

<!-- Paired messages -->
Expand Down Expand Up @@ -161,4 +237,20 @@ const serverInfo = computed(() => {
return null
})

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

</script>
140 changes: 140 additions & 0 deletions mcphawk/stdio_server_detector_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Fallback server detection for stdio transport when initialize message is not available."""

import re
from pathlib import Path
from typing import Optional


def detect_server_from_command(command: list[str]) -> Optional[dict[str, str]]:
"""
Try to detect MCP server info from the command being executed.

This is a fallback when we don't have initialize message info.

Args:
command: Command and arguments list

Returns:
Dict with 'name' and 'version' if detected, None otherwise
"""
if not command:
return None

# Get the executable name
exe = command[0]
exe_name = Path(exe).name
exe_path = Path(exe).stem # without extension

# Check if running Python module with -m
if exe_name in ['python', 'python3', 'python3.exe', 'python.exe']:
# Look for -m module_name pattern
for i, arg in enumerate(command[1:], 1):
if arg == '-m' and i < len(command) - 1:
module = command[i + 1]

# Special case for mcphawk
if module == 'mcphawk' and i + 2 < len(command) and command[i + 2] == 'mcp':
return {'name': 'MCPHawk Query Server', 'version': 'unknown'}

# Extract name from module
name = extract_server_name(module)
if name:
return {'name': name, 'version': 'unknown'}

# Check executable name
name = extract_server_name(exe_path)
if name:
return {'name': name, 'version': 'unknown'}

# Check for .py files in arguments
for arg in command[1:]:
if arg.endswith('.py'):
script_name = Path(arg).stem
name = extract_server_name(script_name)
if name:
return {'name': name, 'version': 'unknown'}

return None


def extract_server_name(text: str) -> Optional[str]:
"""
Extract a human-readable server name from various text patterns.

Args:
text: Text to extract server name from (module name, exe name, etc)

Returns:
Human-readable server name or None
"""
if not text or not isinstance(text, str):
return None

# Pattern 1: mcp-server-{name} or mcp_server_{name}
match = re.match(r'^mcp[-_]server[-_](.+)$', text, re.IGNORECASE)
if match:
name_part = match.group(1)
# Convert to title case, handling both - and _
words = re.split(r'[-_]', name_part)
return f"MCP {' '.join(word.capitalize() for word in words)} Server"

# Pattern 2: {name}-mcp-server or {name}_mcp_server
match = re.match(r'^(.+?)[-_]mcp[-_]server$', text, re.IGNORECASE)
if match:
name_part = match.group(1)
words = re.split(r'[-_]', name_part)
return f"{' '.join(word.capitalize() for word in words)} MCP Server"

# Pattern 3: mcp-{name} or mcp_{name} (but not mcp-server)
match = re.match(r'^mcp[-_](.+)$', text, re.IGNORECASE)
if match:
name_part = match.group(1)
# Skip if it's just "server" without additional parts
if name_part.lower() == 'server':
return None
words = re.split(r'[-_]', name_part)
return f"MCP {' '.join(word.capitalize() for word in words)}"

# Pattern 4: {name}-mcp or {name}_mcp
match = re.match(r'^(.+?)[-_]mcp$', text, re.IGNORECASE)
if match:
name_part = match.group(1)
words = re.split(r'[-_]', name_part)
return f"{' '.join(word.capitalize() for word in words)} MCP"

# Pattern 5: contains 'mcp' somewhere
if 'mcp' in text.lower():
# Clean up and format
words = re.split(r'[-_]', text)
# Filter out empty strings from split
words = [w for w in words if w]
formatted_words = []
for word in words:
if word.lower() == 'mcp':
formatted_words.append('MCP')
else:
formatted_words.append(word.capitalize())
return ' '.join(formatted_words)

return None


def merge_server_info(
detected: Optional[dict[str, str]],
from_protocol: Optional[dict[str, str]]
) -> Optional[dict[str, str]]:
"""
Merge server info from command detection and protocol messages.

Protocol info takes precedence as it's more accurate.

Args:
detected: Server info detected from command
from_protocol: Server info from initialize response

Returns:
Merged server info or None
"""
if from_protocol:
return from_protocol
return detected
14 changes: 10 additions & 4 deletions mcphawk/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
from typing import Optional

from mcphawk.logger import log_message
from mcphawk.stdio_server_detector_fallback import (
detect_server_from_command,
merge_server_info,
)
from mcphawk.web.broadcaster import broadcast_new_log

logger = logging.getLogger(__name__)
Expand All @@ -30,6 +34,7 @@ def __init__(self, command: list[str], debug: bool = False):
self.running = False
self.server_info = None # Track server info from initialize response
self.client_info = None # Track client info from initialize request
self.server_info_fallback = detect_server_from_command(command) # Fallback detection
self.stdin_thread: Optional[threading.Thread] = None
self.stdout_thread: Optional[threading.Thread] = None
self.stderr_thread: Optional[threading.Thread] = None
Expand Down Expand Up @@ -237,10 +242,11 @@ def _log_jsonrpc_message(self, message: dict, direction: str):
"direction": direction
}

# Add server info if we have it
if self.server_info:
metadata["server_name"] = self.server_info["name"]
metadata["server_version"] = self.server_info["version"]
# Merge server info (protocol takes precedence over fallback)
merged_server_info = merge_server_info(self.server_info_fallback, self.server_info)
if merged_server_info:
metadata["server_name"] = merged_server_info["name"]
metadata["server_version"] = merged_server_info["version"]

# Add client info if we have it
if self.client_info:
Expand Down
Loading