Skip to content

Commit 0e129f7

Browse files
author
Alex J Lennon
committed
Add enhanced network map features: device details, alerts, metrics, and layout options
- Add new parameters: layout, group_by, show_details, show_metrics, show_alerts, show_history, export_format - Implement device details display (firmware, MAC, manufacturer, SSH status) - Add alert indicators for SSH errors, high power consumption, devices not seen recently - Add performance metrics visualization with latency-based color coding - Add layout support (lr, tb, radial, hierarchical, grid) - Add CSS classes for latency metrics (excellent/good/fair/poor) and alert devices - Enhance device label building with helper functions for better organization
1 parent 7d5f549 commit 0e129f7

12 files changed

Lines changed: 673 additions & 414 deletions

File tree

call_mcp_protocol.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,66 +11,64 @@
1111
print("Error: MCP client SDK not available")
1212
sys.exit(1)
1313

14+
1415
async def call_mcp_tool():
1516
"""Call create_network_map tool via MCP protocol"""
1617
server_script = Path(__file__).parent / "lab_testing" / "server.py"
1718
project_root = Path(__file__).parent
18-
19+
1920
# Set up server parameters with PYTHONPATH
2021
import os
22+
2123
env = os.environ.copy()
2224
env["PYTHONPATH"] = str(project_root)
23-
25+
2426
server_params = StdioServerParameters(
25-
command=sys.executable,
26-
args=[str(server_script)],
27-
env=env
27+
command=sys.executable, args=[str(server_script)], env=env
2828
)
29-
29+
3030
print("Connecting to MCP server via stdio protocol...")
31-
print("="*70)
32-
31+
print("=" * 70)
32+
3333
async with stdio_client(server_params) as (read, write):
3434
async with ClientSession(read, write) as session:
3535
# Initialize the session
3636
await session.initialize()
37-
37+
3838
# Call the tool
3939
print("\nCalling create_network_map tool...")
4040
result = await session.call_tool(
4141
"create_network_map",
42-
{
43-
"quick_mode": True,
44-
"scan_networks": False,
45-
"test_configured_devices": True
46-
}
42+
{"quick_mode": True, "scan_networks": False, "test_configured_devices": True},
4743
)
48-
44+
4945
print(f"\nTool returned {len(result.content)} content item(s):\n")
50-
46+
5147
# Display results
5248
for i, content in enumerate(result.content, 1):
53-
if hasattr(content, 'text'):
54-
if content.text.startswith('```mermaid'):
49+
if hasattr(content, "text"):
50+
if content.text.startswith("```mermaid"):
5551
print(f"Content {i}: Mermaid Diagram")
56-
print("="*70)
52+
print("=" * 70)
5753
print(content.text)
58-
print("="*70)
54+
print("=" * 70)
5955
else:
6056
print(f"Content {i}: Text")
6157
print(content.text)
62-
elif hasattr(content, 'data'):
58+
elif hasattr(content, "data"):
6359
print(f"Content {i}: Image")
6460
print(f" MIME Type: {content.mimeType}")
6561
print(f" Data length: {len(content.data)} bytes")
66-
62+
6763
return result
6864

65+
6966
if __name__ == "__main__":
7067
try:
7168
result = asyncio.run(call_mcp_tool())
7269
except Exception as e:
7370
print(f"Error: {e}")
7471
import traceback
72+
7573
traceback.print_exc()
7674
sys.exit(1)

call_mcp_tool.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,36 @@
22
"""Call MCP tool directly using the same handler as the MCP server"""
33
import sys
44
import time
5+
6+
from mcp.types import ImageContent, TextContent
7+
58
from lab_testing.server.tool_handlers import handle_tool
6-
from mcp.types import TextContent, ImageContent
79

810
# Simulate MCP tool call exactly as the server would receive it
9-
request_id = 'mcp_request_001'
11+
request_id = "mcp_request_001"
1012
start_time = time.time()
11-
arguments = {
12-
'quick_mode': True,
13-
'scan_networks': False,
14-
'test_configured_devices': True
15-
}
13+
arguments = {"quick_mode": True, "scan_networks": False, "test_configured_devices": True}
1614

17-
print('Calling create_network_map via MCP tool handler...')
18-
print('='*70)
15+
print("Calling create_network_map via MCP tool handler...")
16+
print("=" * 70)
1917

2018
# Call the tool handler (same code path as MCP server uses)
21-
result = handle_tool('create_network_map', arguments, request_id, start_time)
19+
result = handle_tool("create_network_map", arguments, request_id, start_time)
2220

23-
print(f'Returned {len(result)} content item(s):\n')
21+
print(f"Returned {len(result)} content item(s):\n")
2422

2523
# Format output as MCP would return it
2624
for i, content in enumerate(result, 1):
2725
if isinstance(content, TextContent):
28-
if content.text.startswith('```mermaid'):
29-
print(f'Content {i}: TextContent (Mermaid Diagram)')
30-
print('='*70)
26+
if content.text.startswith("```mermaid"):
27+
print(f"Content {i}: TextContent (Mermaid Diagram)")
28+
print("=" * 70)
3129
print(content.text)
32-
print('='*70)
30+
print("=" * 70)
3331
else:
34-
print(f'Content {i}: TextContent')
32+
print(f"Content {i}: TextContent")
3533
print(content.text)
3634
elif isinstance(content, ImageContent):
37-
print(f'Content {i}: ImageContent')
38-
print(f' MIME Type: {content.mimeType}')
39-
print(f' Data length: {len(content.data)} bytes (base64)')
40-
35+
print(f"Content {i}: ImageContent")
36+
print(f" MIME Type: {content.mimeType}")
37+
print(f" Data length: {len(content.data)} bytes (base64)")

lab_testing/server/tool_definitions.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def get_all_tools() -> List[Tool]:
188188
),
189189
Tool(
190190
name="create_network_map",
191-
description="Create a visual map of running systems on the target network showing what's up and what isn't",
191+
description="Create a visual map of running systems on the target network showing what's up and what isn't. Supports multiple layouts, export formats, device grouping, historical tracking, and performance metrics visualization.",
192192
inputSchema={
193193
"type": "object",
194194
"properties": {
@@ -217,6 +217,48 @@ def get_all_tools() -> List[Tool]:
217217
"description": "If true, skip network scanning and only show configured devices (faster, <5s). Use this if tool calls timeout (default: false)",
218218
"default": False,
219219
},
220+
"layout": {
221+
"type": "string",
222+
"enum": ["lr", "tb", "radial", "hierarchical", "grid"],
223+
"description": "Layout style: 'lr' (left-right, default), 'tb' (top-bottom), 'radial' (circular), 'hierarchical' (tree), 'grid' (grid layout)",
224+
"default": "lr",
225+
},
226+
"group_by": {
227+
"type": "string",
228+
"enum": ["type", "status", "location", "power_circuit", "none"],
229+
"description": "Group devices by: 'type' (device type), 'status' (online/offline), 'location' (physical location from config), 'power_circuit' (power switch), 'none' (no grouping)",
230+
"default": "type",
231+
},
232+
"show_details": {
233+
"type": "boolean",
234+
"description": "If true, show detailed device information in node labels (firmware, MAC, last seen, etc.)",
235+
"default": False,
236+
},
237+
"show_metrics": {
238+
"type": "boolean",
239+
"description": "If true, color-code devices by latency and show performance metrics",
240+
"default": True,
241+
},
242+
"show_alerts": {
243+
"type": "boolean",
244+
"description": "If true, highlight devices with errors, warnings, or issues",
245+
"default": True,
246+
},
247+
"show_history": {
248+
"type": "boolean",
249+
"description": "If true, show historical status changes and uptime indicators",
250+
"default": False,
251+
},
252+
"export_format": {
253+
"type": "string",
254+
"enum": ["mermaid", "png", "svg", "pdf", "html", "json", "csv"],
255+
"description": "Export format: 'mermaid' (Mermaid diagram, default), 'png' (PNG image), 'svg' (SVG image), 'pdf' (PDF document), 'html' (HTML report), 'json' (JSON data), 'csv' (CSV device list)",
256+
"default": "mermaid",
257+
},
258+
"export_path": {
259+
"type": "string",
260+
"description": "Optional path to save exported file. If not provided, returns data inline.",
261+
},
220262
},
221263
"required": [],
222264
},

lab_testing/server/tool_handlers.py

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,14 @@ def handle_tool(
785785
test_configured_devices = arguments.get("test_configured_devices", True)
786786
max_hosts = arguments.get("max_hosts_per_network", 254)
787787
quick_mode = arguments.get("quick_mode", False)
788+
layout = arguments.get("layout", "lr")
789+
group_by = arguments.get("group_by", "type")
790+
show_details = arguments.get("show_details", False)
791+
show_metrics = arguments.get("show_metrics", True)
792+
show_alerts = arguments.get("show_alerts", True)
793+
show_history = arguments.get("show_history", False)
794+
export_format = arguments.get("export_format", "mermaid")
795+
export_path = arguments.get("export_path")
788796

789797
# Get target network for display
790798
from lab_testing.config import get_target_network, get_target_network_friendly_name
@@ -802,20 +810,32 @@ def handle_tool(
802810
# Log the operation with target network info
803811
mode_info = "Quick mode (no network scan)" if quick_mode else "Full scan mode"
804812
logger.info(
805-
f"[{request_id}] Creating network map - Target: {target_network}, Mode: {mode_info}"
813+
f"[{request_id}] Creating network map - Target: {target_network}, Mode: {mode_info}, Layout: {layout}, Group by: {group_by}"
806814
)
807815

808816
# Create network map by scanning the network
809817
network_map = create_network_map(
810-
networks, scan_networks, test_configured_devices, max_hosts, quick_mode
818+
networks=networks,
819+
scan_networks=scan_networks,
820+
test_configured_devices=test_configured_devices,
821+
max_hosts_per_network=max_hosts,
822+
quick_mode=quick_mode,
823+
layout=layout,
824+
group_by=group_by,
825+
show_details=show_details,
826+
show_metrics=show_metrics,
827+
show_alerts=show_alerts,
828+
show_history=show_history,
829+
export_format=export_format,
830+
export_path=export_path,
811831
)
812832

813833
# Generate Mermaid diagram (primary)
814834
mermaid_diagram = generate_network_map_mermaid(network_map)
815835

816836
# Convert Mermaid diagram to PNG
817837
mermaid_png_base64 = convert_mermaid_to_png(mermaid_diagram, output_path=None)
818-
838+
819839
# Generate matplotlib PNG image visualization (fallback)
820840
image_base64 = generate_network_map_image(network_map, output_path=None)
821841

@@ -828,7 +848,9 @@ def handle_tool(
828848
"network_map": network_map,
829849
"visualization": visualization,
830850
"mermaid_diagram": mermaid_diagram,
831-
"mermaid_png_base64": mermaid_png_base64[:50] + "..." if mermaid_png_base64 else None,
851+
"mermaid_png_base64": (
852+
mermaid_png_base64[:50] + "..." if mermaid_png_base64 else None
853+
),
832854
"image_base64": image_base64[:50] + "..." if image_base64 else None,
833855
}
834856
_record_tool_result(name, result, request_id, start_time)
@@ -837,42 +859,45 @@ def handle_tool(
837859
contents = []
838860
if mermaid_diagram:
839861
# Return Mermaid diagram as primary TextContent
840-
contents.append(
841-
TextContent(type="text", text=mermaid_diagram)
842-
)
843-
862+
contents.append(TextContent(type="text", text=mermaid_diagram))
863+
844864
# Add PNG image from Mermaid conversion (preferred over matplotlib version)
845865
png_to_use = mermaid_png_base64 if mermaid_png_base64 else image_base64
846-
866+
847867
# Add PNG image as fallback if available
848868
# Try both ImageContent (MCP standard) and data URI in TextContent (for Cursor compatibility)
849869
if png_to_use:
850870
try:
851871
# Return image as ImageContent (MCP standard format)
852-
logger.info(f"[{request_id}] Creating ImageContent: data length={len(png_to_use)}, source={'mermaid' if mermaid_png_base64 else 'matplotlib'}")
853-
image_content = ImageContent(type="image", data=png_to_use, mimeType="image/png")
872+
logger.info(
873+
f"[{request_id}] Creating ImageContent: data length={len(png_to_use)}, source={'mermaid' if mermaid_png_base64 else 'matplotlib'}"
874+
)
875+
image_content = ImageContent(
876+
type="image", data=png_to_use, mimeType="image/png"
877+
)
854878
contents.append(image_content)
855879
logger.info(f"[{request_id}] ImageContent created successfully")
856-
880+
857881
# Save high-resolution image to file for clickable link
858882
import base64
859883
import tempfile
860884
from pathlib import Path
861-
885+
862886
# Save to a file in the project directory for easy access
863887
project_root = Path(__file__).parent.parent.parent
864888
network_map_dir = project_root / "network_maps"
865889
network_map_dir.mkdir(exist_ok=True)
866-
890+
867891
# Create filename with timestamp
868892
from datetime import datetime
893+
869894
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
870895
image_file = network_map_dir / f"network_map_{timestamp}.png"
871-
896+
872897
# Write the PNG data
873898
with open(image_file, "wb") as f:
874899
f.write(base64.b64decode(png_to_use))
875-
900+
876901
# Also add as data URI in TextContent for inline viewing
877902
data_uri = f"data:image/png;base64,{png_to_use}"
878903
# Provide both embedded image and clickable link
@@ -881,17 +906,14 @@ def handle_tool(
881906
f"\n\n"
882907
f'<a href="{data_uri}" target="_blank" title="Click to enlarge">'
883908
f'<img src="{data_uri}" alt="Network Map" style="max-width: 100%; cursor: pointer;" />'
884-
f'</a>\n\n'
909+
f"</a>\n\n"
885910
f"**Full-size image saved to:** `{image_file.relative_to(project_root)}`\n\n"
886911
)
887-
contents.append(
888-
TextContent(
889-
type="text",
890-
text=image_text
891-
)
892-
)
912+
contents.append(TextContent(type="text", text=image_text))
893913
except Exception as e:
894-
logger.error(f"[{request_id}] Failed to create ImageContent: {e}", exc_info=True)
914+
logger.error(
915+
f"[{request_id}] Failed to create ImageContent: {e}", exc_info=True
916+
)
895917
# Fallback: save to temp file and include path
896918
import base64
897919
import tempfile
@@ -905,7 +927,7 @@ def handle_tool(
905927
text=f"Network map image saved to: {tmp_path}\n\n{mermaid_diagram}",
906928
)
907929
)
908-
930+
909931
# If no Mermaid diagram was added, add it as fallback
910932
if not contents and mermaid_diagram:
911933
contents.append(TextContent(type="text", text=mermaid_diagram))

0 commit comments

Comments
 (0)