Skip to content

Commit b70e6cc

Browse files
authored
Merge pull request #4 from tech4242/bugfix/servername-fallback
fix: adding servername fallback for stdio
2 parents e4aa151 + b09ed8c commit b70e6cc

File tree

4 files changed

+418
-5
lines changed

4 files changed

+418
-5
lines changed

frontend/src/components/LogTable/LogRow.vue

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,84 @@
6060
</div>
6161
<!-- Expanded JSON view -->
6262
<div v-if="isExpanded" class="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
63+
<!-- Message metadata -->
64+
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
65+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
66+
<div>
67+
<span class="text-gray-500 dark:text-gray-400">Date:</span>
68+
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
69+
{{ formatDate(log.timestamp) }}
70+
</span>
71+
</div>
72+
<div>
73+
<span class="text-gray-500 dark:text-gray-400">Time:</span>
74+
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
75+
{{ formatTimestamp(log.timestamp) }}
76+
</span>
77+
</div>
78+
<div>
79+
<span class="text-gray-500 dark:text-gray-400">Type:</span>
80+
<MessageTypeBadge :type="messageType" class="ml-2" />
81+
</div>
82+
<div>
83+
<span class="text-gray-500 dark:text-gray-400">Message ID:</span>
84+
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
85+
{{ messageId }}
86+
</span>
87+
</div>
88+
<div>
89+
<span class="text-gray-500 dark:text-gray-400">Server:</span>
90+
<span v-if="serverInfo" class="ml-2 text-gray-900 dark:text-gray-100">
91+
{{ serverInfo.name }} <span class="text-gray-500 dark:text-gray-400">v{{ serverInfo.version }}</span>
92+
</span>
93+
<span v-else class="ml-2 text-gray-500 dark:text-gray-400">-</span>
94+
</div>
95+
<div v-if="clientInfo">
96+
<span class="text-gray-500 dark:text-gray-400">Client:</span>
97+
<span class="ml-2 text-gray-900 dark:text-gray-100">
98+
{{ clientInfo.name }} <span class="text-gray-500 dark:text-gray-400">v{{ clientInfo.version }}</span>
99+
</span>
100+
</div>
101+
<div>
102+
<span class="text-gray-500 dark:text-gray-400">Transport:</span>
103+
<span
104+
class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
105+
:class="transportTypeColor"
106+
>
107+
{{ formattedTransportType }}
108+
</span>
109+
</div>
110+
<div>
111+
<span class="text-gray-500 dark:text-gray-400">Direction:</span>
112+
<span class="ml-2 text-gray-900 dark:text-gray-100">
113+
{{ log.src_ip }} {{ directionIcon }} {{ log.dst_ip }}
114+
</span>
115+
</div>
116+
<div v-if="log.src_port || log.dst_port">
117+
<span class="text-gray-500 dark:text-gray-400">Ports:</span>
118+
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
119+
{{ portInfo }}
120+
</span>
121+
</div>
122+
<div v-if="log.pid">
123+
<span class="text-gray-500 dark:text-gray-400">PID:</span>
124+
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100">
125+
{{ log.pid }}
126+
</span>
127+
</div>
128+
<div v-if="log.log_id">
129+
<span class="text-gray-500 dark:text-gray-400">Log ID:</span>
130+
<span class="ml-2 font-mono text-gray-900 dark:text-gray-100 text-xs">
131+
{{ log.log_id }}
132+
</span>
133+
</div>
134+
</div>
135+
</div>
136+
137+
<!-- JSON content -->
63138
<div class="px-4 py-3">
64-
<pre class="text-xs font-mono text-gray-800 dark:text-gray-200 overflow-x-auto">{{ formattedJson }}</pre>
139+
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">JSON-RPC Message:</div>
140+
<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>
65141
</div>
66142

67143
<!-- Paired messages -->
@@ -161,4 +237,20 @@ const serverInfo = computed(() => {
161237
return null
162238
})
163239
240+
const clientInfo = computed(() => {
241+
if (!props.log.metadata) return null
242+
try {
243+
const meta = JSON.parse(props.log.metadata)
244+
if (meta.client_name) {
245+
return {
246+
name: meta.client_name,
247+
version: meta.client_version || ''
248+
}
249+
}
250+
} catch {
251+
// ignore
252+
}
253+
return null
254+
})
255+
164256
</script>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Fallback server detection for stdio transport when initialize message is not available."""
2+
3+
import re
4+
from pathlib import Path
5+
from typing import Optional
6+
7+
8+
def detect_server_from_command(command: list[str]) -> Optional[dict[str, str]]:
9+
"""
10+
Try to detect MCP server info from the command being executed.
11+
12+
This is a fallback when we don't have initialize message info.
13+
14+
Args:
15+
command: Command and arguments list
16+
17+
Returns:
18+
Dict with 'name' and 'version' if detected, None otherwise
19+
"""
20+
if not command:
21+
return None
22+
23+
# Get the executable name
24+
exe = command[0]
25+
exe_name = Path(exe).name
26+
exe_path = Path(exe).stem # without extension
27+
28+
# Check if running Python module with -m
29+
if exe_name in ['python', 'python3', 'python3.exe', 'python.exe']:
30+
# Look for -m module_name pattern
31+
for i, arg in enumerate(command[1:], 1):
32+
if arg == '-m' and i < len(command) - 1:
33+
module = command[i + 1]
34+
35+
# Special case for mcphawk
36+
if module == 'mcphawk' and i + 2 < len(command) and command[i + 2] == 'mcp':
37+
return {'name': 'MCPHawk Query Server', 'version': 'unknown'}
38+
39+
# Extract name from module
40+
name = extract_server_name(module)
41+
if name:
42+
return {'name': name, 'version': 'unknown'}
43+
44+
# Check executable name
45+
name = extract_server_name(exe_path)
46+
if name:
47+
return {'name': name, 'version': 'unknown'}
48+
49+
# Check for .py files in arguments
50+
for arg in command[1:]:
51+
if arg.endswith('.py'):
52+
script_name = Path(arg).stem
53+
name = extract_server_name(script_name)
54+
if name:
55+
return {'name': name, 'version': 'unknown'}
56+
57+
return None
58+
59+
60+
def extract_server_name(text: str) -> Optional[str]:
61+
"""
62+
Extract a human-readable server name from various text patterns.
63+
64+
Args:
65+
text: Text to extract server name from (module name, exe name, etc)
66+
67+
Returns:
68+
Human-readable server name or None
69+
"""
70+
if not text or not isinstance(text, str):
71+
return None
72+
73+
# Pattern 1: mcp-server-{name} or mcp_server_{name}
74+
match = re.match(r'^mcp[-_]server[-_](.+)$', text, re.IGNORECASE)
75+
if match:
76+
name_part = match.group(1)
77+
# Convert to title case, handling both - and _
78+
words = re.split(r'[-_]', name_part)
79+
return f"MCP {' '.join(word.capitalize() for word in words)} Server"
80+
81+
# Pattern 2: {name}-mcp-server or {name}_mcp_server
82+
match = re.match(r'^(.+?)[-_]mcp[-_]server$', text, re.IGNORECASE)
83+
if match:
84+
name_part = match.group(1)
85+
words = re.split(r'[-_]', name_part)
86+
return f"{' '.join(word.capitalize() for word in words)} MCP Server"
87+
88+
# Pattern 3: mcp-{name} or mcp_{name} (but not mcp-server)
89+
match = re.match(r'^mcp[-_](.+)$', text, re.IGNORECASE)
90+
if match:
91+
name_part = match.group(1)
92+
# Skip if it's just "server" without additional parts
93+
if name_part.lower() == 'server':
94+
return None
95+
words = re.split(r'[-_]', name_part)
96+
return f"MCP {' '.join(word.capitalize() for word in words)}"
97+
98+
# Pattern 4: {name}-mcp or {name}_mcp
99+
match = re.match(r'^(.+?)[-_]mcp$', text, re.IGNORECASE)
100+
if match:
101+
name_part = match.group(1)
102+
words = re.split(r'[-_]', name_part)
103+
return f"{' '.join(word.capitalize() for word in words)} MCP"
104+
105+
# Pattern 5: contains 'mcp' somewhere
106+
if 'mcp' in text.lower():
107+
# Clean up and format
108+
words = re.split(r'[-_]', text)
109+
# Filter out empty strings from split
110+
words = [w for w in words if w]
111+
formatted_words = []
112+
for word in words:
113+
if word.lower() == 'mcp':
114+
formatted_words.append('MCP')
115+
else:
116+
formatted_words.append(word.capitalize())
117+
return ' '.join(formatted_words)
118+
119+
return None
120+
121+
122+
def merge_server_info(
123+
detected: Optional[dict[str, str]],
124+
from_protocol: Optional[dict[str, str]]
125+
) -> Optional[dict[str, str]]:
126+
"""
127+
Merge server info from command detection and protocol messages.
128+
129+
Protocol info takes precedence as it's more accurate.
130+
131+
Args:
132+
detected: Server info detected from command
133+
from_protocol: Server info from initialize response
134+
135+
Returns:
136+
Merged server info or None
137+
"""
138+
if from_protocol:
139+
return from_protocol
140+
return detected

mcphawk/wrapper.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
from typing import Optional
1616

1717
from mcphawk.logger import log_message
18+
from mcphawk.stdio_server_detector_fallback import (
19+
detect_server_from_command,
20+
merge_server_info,
21+
)
1822
from mcphawk.web.broadcaster import broadcast_new_log
1923

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

240-
# Add server info if we have it
241-
if self.server_info:
242-
metadata["server_name"] = self.server_info["name"]
243-
metadata["server_version"] = self.server_info["version"]
245+
# Merge server info (protocol takes precedence over fallback)
246+
merged_server_info = merge_server_info(self.server_info_fallback, self.server_info)
247+
if merged_server_info:
248+
metadata["server_name"] = merged_server_info["name"]
249+
metadata["server_version"] = merged_server_info["version"]
244250

245251
# Add client info if we have it
246252
if self.client_info:

0 commit comments

Comments
 (0)