#
tokens: 21716/50000 3/36 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 3. Use http://codebase.md/always-tinkering/rhinomcpserver?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── code_architecture.md
├── combined_mcp_server.py
├── diagnose_rhino_connection.py
├── log_manager.py
├── LOGGING.md
├── logs
│   └── json_filter.py
├── mcpLLM.txt
├── NLog.config
├── README.md
├── RHINO_PLUGIN_UPDATE.md
├── RhinoMcpPlugin
│   ├── Models
│   │   └── RhinoObjectProperties.cs
│   ├── Properties
│   │   └── AssemblyInfo.cs
│   ├── RhinoMcpCommand.cs
│   ├── RhinoMcpPlugin.cs
│   ├── RhinoMcpPlugin.csproj
│   ├── RhinoSocketServer.cs
│   ├── RhinoUtilities.cs
│   └── Tools
│       ├── GeometryTools.cs
│       └── SceneTools.cs
├── RhinoMcpPlugin.Tests
│   ├── ImplementationPlan.md
│   ├── Mocks
│   │   └── MockRhinoDoc.cs
│   ├── README.md
│   ├── RhinoMcpPlugin.Tests.csproj
│   ├── test.runsettings
│   ├── Tests
│   │   ├── ColorUtilTests.cs
│   │   ├── RhinoMcpPluginTests.cs
│   │   ├── RhinoSocketServerTests.cs
│   │   └── RhinoUtilitiesTests.cs
│   └── Utils
│       └── ColorParser.cs
├── RhinoPluginFixImplementation.cs
├── RhinoPluginLoggingSpec.md
├── run-combined-server.sh
├── scripts
│   ├── direct-launcher.sh
│   ├── run-combined-server.sh
│   └── run-python-server.sh
└── src
    ├── daemon_mcp_server.py
    ├── socket_proxy.py
    └── standalone-mcp-server.py
```

# Files

--------------------------------------------------------------------------------
/log_manager.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Log Manager Utility for RhinoMcpServer
  4 | 
  5 | This script helps manage, view, and analyze logs from the MCP Server,
  6 | Rhino plugin, Claude AI, and diagnostic tools. It provides a unified
  7 | view of logs across all components to aid in debugging.
  8 | """
  9 | 
 10 | import os
 11 | import sys
 12 | import re
 13 | import glob
 14 | import argparse
 15 | from datetime import datetime, timedelta
 16 | import json
 17 | import subprocess
 18 | 
 19 | # Define the log directory structure
 20 | LOG_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
 21 | SERVER_LOGS = os.path.join(LOG_ROOT, "server")
 22 | PLUGIN_LOGS = os.path.join(LOG_ROOT, "plugin")
 23 | CLAUDE_LOGS = os.path.join(LOG_ROOT, "claude")
 24 | DIAGNOSTIC_LOGS = os.path.join(LOG_ROOT, "diagnostics")
 25 | 
 26 | # Log entry pattern for parsing - matches standard timestamp format
 27 | LOG_PATTERN = re.compile(r'^\[(?P<timestamp>.*?)\] \[(?P<level>.*?)\] \[(?P<component>.*?)\] (?P<message>.*)$')
 28 | 
 29 | class LogEntry:
 30 |     """Represents a parsed log entry with timestamp and metadata"""
 31 |     
 32 |     def __init__(self, timestamp, level, component, message, source_file):
 33 |         self.timestamp = timestamp
 34 |         self.level = level.upper()
 35 |         self.component = component
 36 |         self.message = message
 37 |         self.source_file = source_file
 38 |     
 39 |     @classmethod
 40 |     def from_line(cls, line, source_file):
 41 |         """Parse a log line into a LogEntry object"""
 42 |         match = LOG_PATTERN.match(line)
 43 |         if match:
 44 |             try:
 45 |                 timestamp = datetime.strptime(match.group('timestamp'), '%Y-%m-%d %H:%M:%S,%f')
 46 |             except ValueError:
 47 |                 try:
 48 |                     timestamp = datetime.strptime(match.group('timestamp'), '%Y-%m-%d %H:%M:%S')
 49 |                 except ValueError:
 50 |                     # If timestamp can't be parsed, use current time
 51 |                     timestamp = datetime.now()
 52 |             
 53 |             return cls(
 54 |                 timestamp, 
 55 |                 match.group('level'), 
 56 |                 match.group('component'), 
 57 |                 match.group('message'),
 58 |                 source_file
 59 |             )
 60 |         return None
 61 | 
 62 |     def __lt__(self, other):
 63 |         """Support sorting by timestamp"""
 64 |         return self.timestamp < other.timestamp
 65 |     
 66 |     def to_string(self, colors=True, show_source=False):
 67 |         """Format the log entry for display"""
 68 |         # Define ANSI color codes
 69 |         color_map = {
 70 |             "DEBUG": "\033[36m",  # Cyan
 71 |             "INFO": "\033[32m",   # Green
 72 |             "WARNING": "\033[33m", # Yellow
 73 |             "ERROR": "\033[31m",  # Red
 74 |             "CRITICAL": "\033[41m\033[97m"  # White on red background
 75 |         }
 76 |         component_colors = {
 77 |             "server": "\033[94m",  # Blue
 78 |             "plugin": "\033[95m",  # Magenta
 79 |             "diagnostic": "\033[96m",  # Cyan
 80 |             "claude": "\033[92m"   # Green
 81 |         }
 82 |         reset = "\033[0m"
 83 |         
 84 |         # Format timestamp
 85 |         timestamp_str = self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
 86 |         
 87 |         # Apply colors if enabled
 88 |         if colors:
 89 |             level_color = color_map.get(self.level, "") 
 90 |             comp_color = component_colors.get(self.component, "")
 91 |             
 92 |             if self.level in ["ERROR", "CRITICAL"]:
 93 |                 # For errors, color the whole line
 94 |                 source_info = f" ({os.path.basename(self.source_file)})" if show_source else ""
 95 |                 return f"{level_color}[{timestamp_str}] [{self.level}] [{self.component}] {self.message}{source_info}{reset}"
 96 |             else:
 97 |                 # For non-errors, color just the level and component
 98 |                 source_info = f" ({os.path.basename(self.source_file)})" if show_source else ""
 99 |                 return f"[{timestamp_str}] [{level_color}{self.level}{reset}] [{comp_color}{self.component}{reset}] {self.message}{source_info}"
100 |         else:
101 |             source_info = f" ({os.path.basename(self.source_file)})" if show_source else ""
102 |             return f"[{timestamp_str}] [{self.level}] [{self.component}] {self.message}{source_info}"
103 | 
104 | def collect_logs(since=None, level_filter=None, component_filter=None):
105 |     """Collect and parse logs from all sources
106 |     
107 |     Args:
108 |         since: Datetime object for filtering logs by age
109 |         level_filter: List of log levels to include (DEBUG, INFO, etc)
110 |         component_filter: List of components to include (server, plugin, etc)
111 |     
112 |     Returns:
113 |         List of LogEntry objects sorted by timestamp
114 |     """
115 |     all_entries = []
116 |     
117 |     # Create directories if they don't exist
118 |     for directory in [LOG_ROOT, SERVER_LOGS, PLUGIN_LOGS, CLAUDE_LOGS, DIAGNOSTIC_LOGS]:
119 |         os.makedirs(directory, exist_ok=True)
120 |     
121 |     # Gather log files from all directories
122 |     log_files = []
123 |     log_files.extend(glob.glob(os.path.join(SERVER_LOGS, "*.log")))
124 |     log_files.extend(glob.glob(os.path.join(PLUGIN_LOGS, "*.log")))
125 |     log_files.extend(glob.glob(os.path.join(DIAGNOSTIC_LOGS, "*.log")))
126 |     
127 |     # Also check for Claude logs but handle them differently
128 |     claude_files = glob.glob(os.path.join(CLAUDE_LOGS, "*.log"))
129 |     
130 |     # Process standard log files
131 |     for log_file in log_files:
132 |         try:
133 |             with open(log_file, 'r') as f:
134 |                 for line in f:
135 |                     line = line.strip()
136 |                     if not line:
137 |                         continue
138 |                     
139 |                     entry = LogEntry.from_line(line, log_file)
140 |                     if entry:
141 |                         # Apply filters
142 |                         if since and entry.timestamp < since:
143 |                             continue
144 |                         if level_filter and entry.level not in level_filter:
145 |                             continue
146 |                         if component_filter and entry.component not in component_filter:
147 |                             continue
148 |                         
149 |                         all_entries.append(entry)
150 |         except Exception as e:
151 |             print(f"Error processing {log_file}: {e}", file=sys.stderr)
152 |     
153 |     # Handle Claude logs which have a different format
154 |     for claude_file in claude_files:
155 |         try:
156 |             # Get file creation time to use as timestamp
157 |             file_time = datetime.fromtimestamp(os.path.getctime(claude_file))
158 |             
159 |             # Skip if it's before our filter time
160 |             if since and file_time < since:
161 |                 continue
162 |             
163 |             # Only include preview of Claude logs since they can be large
164 |             with open(claude_file, 'r') as f:
165 |                 content = f.read(500)  # Just read first 500 chars
166 |                 truncated = len(content) < os.path.getsize(claude_file)
167 |                 message = content + ("..." if truncated else "")
168 |                 
169 |                 # Create a synthetic log entry
170 |                 entry = LogEntry(
171 |                     file_time,
172 |                     "INFO", 
173 |                     "claude", 
174 |                     f"Claude interaction: {message}",
175 |                     claude_file
176 |                 )
177 |                 
178 |                 # Apply filters
179 |                 if not component_filter or "claude" in component_filter:
180 |                     all_entries.append(entry)
181 |         except Exception as e:
182 |             print(f"Error processing Claude log {claude_file}: {e}", file=sys.stderr)
183 |     
184 |     # Sort all entries by timestamp
185 |     all_entries.sort()
186 |     return all_entries
187 | 
188 | def display_logs(entries, colors=True, show_source=False, max_entries=None):
189 |     """Display log entries with optional formatting
190 |     
191 |     Args:
192 |         entries: List of LogEntry objects
193 |         colors: Whether to use ANSI colors in output
194 |         show_source: Whether to show source filename
195 |         max_entries: Maximum number of entries to show (None for all)
196 |     """
197 |     # Maybe limit entries
198 |     if max_entries is not None and len(entries) > max_entries:
199 |         skipped = len(entries) - max_entries
200 |         entries = entries[-max_entries:]
201 |         print(f"... (skipped {skipped} earlier entries) ...\n")
202 |     
203 |     for entry in entries:
204 |         print(entry.to_string(colors=colors, show_source=show_source))
205 |         
206 | def extract_error_context(entries, context_lines=5):
207 |     """Extract log entries around errors with context
208 |     
209 |     Args:
210 |         entries: List of all LogEntry objects
211 |         context_lines: Number of log lines before and after each error
212 |         
213 |     Returns:
214 |         List of error contexts (each being a list of LogEntry objects)
215 |     """
216 |     error_contexts = []
217 |     error_indices = [i for i, entry in enumerate(entries) if entry.level in ["ERROR", "CRITICAL"]]
218 |     
219 |     for error_idx in error_indices:
220 |         # Get context before and after error
221 |         start_idx = max(0, error_idx - context_lines)
222 |         end_idx = min(len(entries), error_idx + context_lines + 1)
223 |         
224 |         # Extract the context
225 |         context = entries[start_idx:end_idx]
226 |         error_contexts.append(context)
227 |     
228 |     return error_contexts
229 | 
230 | def generate_error_report(entries):
231 |     """Generate a summary report of errors
232 |     
233 |     Args:
234 |         entries: List of LogEntry objects
235 |         
236 |     Returns:
237 |         String containing the error report
238 |     """
239 |     error_entries = [e for e in entries if e.level in ["ERROR", "CRITICAL"]]
240 |     
241 |     if not error_entries:
242 |         return "No errors found in logs."
243 |     
244 |     # Group errors by component
245 |     errors_by_component = {}
246 |     for entry in error_entries:
247 |         if entry.component not in errors_by_component:
248 |             errors_by_component[entry.component] = []
249 |         errors_by_component[entry.component].append(entry)
250 |     
251 |     # Generate the report
252 |     report = f"Error Report ({len(error_entries)} errors found)\n"
253 |     report += "=" * 50 + "\n\n"
254 |     
255 |     for component, errors in errors_by_component.items():
256 |         report += f"{component.upper()} Errors: {len(errors)}\n"
257 |         report += "-" * 30 + "\n"
258 |         
259 |         # Group by error message pattern
260 |         error_patterns = {}
261 |         for error in errors:
262 |             # Simplify message by removing variable parts (numbers, IDs, etc)
263 |             simplified = re.sub(r'\b(?:\w+[-_])?[0-9a-f]{8}(?:[-_]\w+)?\b', 'ID', error.message)
264 |             simplified = re.sub(r'\d+', 'N', simplified)
265 |             
266 |             if simplified not in error_patterns:
267 |                 error_patterns[simplified] = []
268 |             error_patterns[simplified].append(error)
269 |         
270 |         # Report each error pattern
271 |         for pattern, pattern_errors in error_patterns.items():
272 |             report += f"\n• {pattern} ({len(pattern_errors)} occurrences)\n"
273 |             report += f"  First seen: {pattern_errors[0].timestamp}\n"
274 |             report += f"  Last seen: {pattern_errors[-1].timestamp}\n"
275 |             report += f"  Example: {pattern_errors[-1].message}\n"
276 |         
277 |         report += "\n" + "=" * 50 + "\n\n"
278 |     
279 |     return report
280 | 
281 | def clear_logs(days_old=None, component=None, confirm=True):
282 |     """Clear logs based on specified criteria
283 |     
284 |     Args:
285 |         days_old: Delete logs older than this many days
286 |         component: Only delete logs from this component
287 |         confirm: Whether to prompt for confirmation
288 |     """
289 |     # Determine which directories to clean
290 |     dirs_to_clean = []
291 |     if component == "server" or component is None:
292 |         dirs_to_clean.append(SERVER_LOGS)
293 |     if component == "plugin" or component is None:
294 |         dirs_to_clean.append(PLUGIN_LOGS)
295 |     if component == "claude" or component is None:
296 |         dirs_to_clean.append(CLAUDE_LOGS)
297 |     if component == "diagnostic" or component is None:
298 |         dirs_to_clean.append(DIAGNOSTIC_LOGS)
299 |     
300 |     # Collect files to delete
301 |     files_to_delete = []
302 |     for directory in dirs_to_clean:
303 |         for log_file in glob.glob(os.path.join(directory, "*.log")):
304 |             if days_old is not None:
305 |                 # Check file age
306 |                 file_time = datetime.fromtimestamp(os.path.getmtime(log_file))
307 |                 cutoff_time = datetime.now() - timedelta(days=days_old)
308 |                 if file_time >= cutoff_time:
309 |                     continue  # Skip files newer than cutoff
310 |             
311 |             files_to_delete.append(log_file)
312 |     
313 |     # Nothing to delete
314 |     if not files_to_delete:
315 |         print("No logs found matching the specified criteria.")
316 |         return
317 |     
318 |     # Confirm deletion
319 |     if confirm:
320 |         print(f"Will delete {len(files_to_delete)} log files:")
321 |         for f in files_to_delete[:5]:
322 |             print(f"  - {os.path.basename(f)}")
323 |         
324 |         if len(files_to_delete) > 5:
325 |             print(f"  - ... and {len(files_to_delete) - 5} more")
326 |         
327 |         confirmation = input("Proceed with deletion? (y/N): ").lower()
328 |         if confirmation != 'y':
329 |             print("Deletion cancelled.")
330 |             return
331 |     
332 |     # Delete the files
333 |     deleted_count = 0
334 |     for log_file in files_to_delete:
335 |         try:
336 |             os.remove(log_file)
337 |             deleted_count += 1
338 |         except Exception as e:
339 |             print(f"Error deleting {log_file}: {e}")
340 |     
341 |     print(f"Successfully deleted {deleted_count} log files.")
342 | 
343 | def monitor_logs(interval=1.0, colors=True, level_filter=None, component_filter=None):
344 |     """Monitor logs in real-time like 'tail -f'
345 |     
346 |     Args:
347 |         interval: Polling interval in seconds
348 |         colors: Whether to use ANSI colors
349 |         level_filter: Optional list of log levels to show
350 |         component_filter: Optional list of components to show
351 |     """
352 |     print(f"Monitoring logs (Ctrl+C to exit)...")
353 |     print(f"Filters: levels={level_filter or 'all'}, components={component_filter or 'all'}")
354 |     
355 |     # Get initial log entries and remember the latest timestamp
356 |     entries = collect_logs(level_filter=level_filter, component_filter=component_filter)
357 |     last_timestamp = entries[-1].timestamp if entries else datetime.now()
358 |     
359 |     try:
360 |         while True:
361 |             # Wait for the specified interval
362 |             sys.stdout.flush()
363 |             subprocess.call("", shell=True)  # Hack to make ANSI colors work in Windows
364 |             
365 |             # Get new entries since the last check
366 |             new_entries = collect_logs(
367 |                 since=last_timestamp, 
368 |                 level_filter=level_filter,
369 |                 component_filter=component_filter
370 |             )
371 |             
372 |             # Update timestamp for the next iteration
373 |             if new_entries:
374 |                 last_timestamp = new_entries[-1].timestamp
375 |                 
376 |                 # Display new entries
377 |                 for entry in new_entries:
378 |                     print(entry.to_string(colors=colors, show_source=True))
379 |             
380 |             # Sleep before checking again
381 |             import time
382 |             time.sleep(interval)
383 |     
384 |     except KeyboardInterrupt:
385 |         print("\nStopped monitoring logs.")
386 | 
387 | def main():
388 |     """Parse arguments and execute requested command"""
389 |     parser = argparse.ArgumentParser(description="Manage and view RhinoMcpServer logs")
390 |     
391 |     subparsers = parser.add_subparsers(dest="command", help="Command to execute")
392 |     
393 |     # view command
394 |     view_parser = subparsers.add_parser("view", help="View logs")
395 |     view_parser.add_argument("--since", type=str, help="Show logs since (e.g. '1h', '2d', '30m')")
396 |     view_parser.add_argument("--level", type=str, help="Filter by log level (comma-separated: DEBUG,INFO,WARNING,ERROR)")
397 |     view_parser.add_argument("--component", type=str, help="Filter by component (comma-separated: server,plugin,claude,diagnostic)")
398 |     view_parser.add_argument("--no-color", action="store_true", help="Disable colored output")
399 |     view_parser.add_argument("--source", action="store_true", help="Show source log file")
400 |     view_parser.add_argument("--max", type=int, help="Maximum number of entries to display")
401 |     
402 |     # errors command
403 |     errors_parser = subparsers.add_parser("errors", help="View errors with context")
404 |     errors_parser.add_argument("--since", type=str, help="Show errors since (e.g. '1h', '2d', '30m')")
405 |     errors_parser.add_argument("--component", type=str, help="Filter by component")
406 |     errors_parser.add_argument("--context", type=int, default=5, help="Number of context lines before/after each error")
407 |     errors_parser.add_argument("--no-color", action="store_true", help="Disable colored output")
408 |     
409 |     # report command
410 |     report_parser = subparsers.add_parser("report", help="Generate error report")
411 |     report_parser.add_argument("--since", type=str, help="Include errors since (e.g. '1h', '2d', '30m')")
412 |     report_parser.add_argument("--output", type=str, help="Output file for the report")
413 |     
414 |     # clear command
415 |     clear_parser = subparsers.add_parser("clear", help="Clear logs")
416 |     clear_parser.add_argument("--older-than", type=int, help="Delete logs older than N days")
417 |     clear_parser.add_argument("--component", type=str, help="Only clear logs for the specified component")
418 |     clear_parser.add_argument("--force", action="store_true", help="Do not ask for confirmation")
419 |     
420 |     # monitor command
421 |     monitor_parser = subparsers.add_parser("monitor", help="Monitor logs in real-time")
422 |     monitor_parser.add_argument("--interval", type=float, default=1.0, help="Polling interval in seconds")
423 |     monitor_parser.add_argument("--level", type=str, help="Filter by log level (comma-separated)")
424 |     monitor_parser.add_argument("--component", type=str, help="Filter by component (comma-separated)")
425 |     monitor_parser.add_argument("--no-color", action="store_true", help="Disable colored output")
426 |     
427 |     # info command
428 |     info_parser = subparsers.add_parser("info", help="Show information about logs")
429 |     
430 |     args = parser.parse_args()
431 |     
432 |     # Handle time expressions like "1h", "2d", etc.
433 |     since_time = None
434 |     if hasattr(args, 'since') and args.since:
435 |         try:
436 |             # Parse time expressions
437 |             value = int(args.since[:-1])
438 |             unit = args.since[-1].lower()
439 |             
440 |             if unit == 'h':
441 |                 since_time = datetime.now() - timedelta(hours=value)
442 |             elif unit == 'm':
443 |                 since_time = datetime.now() - timedelta(minutes=value)
444 |             elif unit == 'd':
445 |                 since_time = datetime.now() - timedelta(days=value)
446 |             else:
447 |                 print(f"Invalid time unit in '{args.since}'. Use 'm' for minutes, 'h' for hours, 'd' for days.")
448 |                 return 1
449 |         except ValueError:
450 |             print(f"Invalid time format: '{args.since}'. Use e.g. '1h', '30m', '2d'")
451 |             return 1
452 |     
453 |     # Parse level and component filters
454 |     level_filter = None
455 |     if hasattr(args, 'level') and args.level:
456 |         level_filter = [l.strip().upper() for l in args.level.split(',')]
457 |     
458 |     component_filter = None
459 |     if hasattr(args, 'component') and args.component:
460 |         component_filter = [c.strip().lower() for c in args.component.split(',')]
461 |     
462 |     # Execute the requested command
463 |     if args.command == "view":
464 |         entries = collect_logs(since=since_time, level_filter=level_filter, component_filter=component_filter)
465 |         if not entries:
466 |             print("No log entries found matching the criteria.")
467 |             return 0
468 |         
469 |         display_logs(
470 |             entries, 
471 |             colors=not args.no_color, 
472 |             show_source=args.source, 
473 |             max_entries=args.max
474 |         )
475 |     
476 |     elif args.command == "errors":
477 |         # Collect all entries first
478 |         all_entries = collect_logs(since=since_time, component_filter=component_filter)
479 |         if not all_entries:
480 |             print("No log entries found.")
481 |             return 0
482 |         
483 |         # Extract errors with context
484 |         error_contexts = extract_error_context(all_entries, context_lines=args.context)
485 |         if not error_contexts:
486 |             print("No errors found in the logs.")
487 |             return 0
488 |         
489 |         # Display each error with its context
490 |         for i, context in enumerate(error_contexts):
491 |             if i > 0:
492 |                 print("\n" + "-" * 80 + "\n")
493 |             
494 |             print(f"Error {i+1} of {len(error_contexts)}:")
495 |             for entry in context:
496 |                 print(entry.to_string(colors=not args.no_color, show_source=True))
497 |     
498 |     elif args.command == "report":
499 |         entries = collect_logs(since=since_time)
500 |         if not entries:
501 |             print("No log entries found.")
502 |             return 0
503 |         
504 |         report = generate_error_report(entries)
505 |         
506 |         if args.output:
507 |             with open(args.output, 'w') as f:
508 |                 f.write(report)
509 |             print(f"Error report saved to {args.output}")
510 |         else:
511 |             print(report)
512 |     
513 |     elif args.command == "clear":
514 |         clear_logs(
515 |             days_old=args.older_than,
516 |             component=args.component,
517 |             confirm=not args.force
518 |         )
519 |     
520 |     elif args.command == "monitor":
521 |         monitor_logs(
522 |             interval=args.interval,
523 |             colors=not args.no_color,
524 |             level_filter=level_filter,
525 |             component_filter=component_filter
526 |         )
527 |     
528 |     elif args.command == "info":
529 |         # Show information about available logs
530 |         print("Log Directory Structure:")
531 |         print(f"  Root: {LOG_ROOT}")
532 |         
533 |         # Helper function to summarize logs in a directory
534 |         def summarize_dir(dir_path, name):
535 |             if not os.path.exists(dir_path):
536 |                 return f"{name}: Directory not found"
537 |             
538 |             log_files = glob.glob(os.path.join(dir_path, "*.log"))
539 |             if not log_files:
540 |                 return f"{name}: No log files found"
541 |             
542 |             newest = max(log_files, key=os.path.getmtime)
543 |             oldest = min(log_files, key=os.path.getmtime)
544 |             newest_time = datetime.fromtimestamp(os.path.getmtime(newest))
545 |             oldest_time = datetime.fromtimestamp(os.path.getmtime(oldest))
546 |             
547 |             total_size = sum(os.path.getsize(f) for f in log_files)
548 |             size_mb = total_size / (1024 * 1024)
549 |             
550 |             return (f"{name}: {len(log_files)} files, {size_mb:.2f} MB total\n"
551 |                     f"  Newest: {os.path.basename(newest)} ({newest_time})\n"
552 |                     f"  Oldest: {os.path.basename(oldest)} ({oldest_time})")
553 |         
554 |         print("\nLog Summaries:")
555 |         print(summarize_dir(SERVER_LOGS, "Server Logs"))
556 |         print(summarize_dir(PLUGIN_LOGS, "Plugin Logs"))
557 |         print(summarize_dir(CLAUDE_LOGS, "Claude Logs"))
558 |         print(summarize_dir(DIAGNOSTIC_LOGS, "Diagnostic Logs"))
559 |     
560 |     else:
561 |         parser.print_help()
562 |     
563 |     return 0
564 | 
565 | if __name__ == "__main__":
566 |     sys.exit(main()) 
```

--------------------------------------------------------------------------------
/combined_mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Combined MCP Server for Rhino
  4 | This script implements a direct MCP server using the FastMCP pattern,
  5 | following the Model Context Protocol specification.
  6 | """
  7 | 
  8 | import json
  9 | import os
 10 | import sys
 11 | import time
 12 | import logging
 13 | import signal
 14 | import threading
 15 | import traceback
 16 | from datetime import datetime
 17 | import re
 18 | import uuid
 19 | import asyncio
 20 | import socket
 21 | from typing import Dict, Any, List, Optional, Tuple
 22 | from contextlib import asynccontextmanager
 23 | from typing import AsyncIterator
 24 | 
 25 | # Import the FastMCP class
 26 | from mcp.server.fastmcp import FastMCP, Context
 27 | 
 28 | # Configure logging - improved with structured format and unified location
 29 | log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
 30 | os.makedirs(log_dir, exist_ok=True)
 31 | 
 32 | # Create log subdirectories
 33 | server_log_dir = os.path.join(log_dir, "server")
 34 | plugin_log_dir = os.path.join(log_dir, "plugin")
 35 | claude_log_dir = os.path.join(log_dir, "claude")
 36 | 
 37 | for directory in [server_log_dir, plugin_log_dir, claude_log_dir]:
 38 |     os.makedirs(directory, exist_ok=True)
 39 | 
 40 | # Log filenames based on date for easier archiving
 41 | today = datetime.now().strftime("%Y-%m-%d")
 42 | server_log_file = os.path.join(server_log_dir, f"server_{today}.log")
 43 | debug_log_file = os.path.join(server_log_dir, f"debug_{today}.log")
 44 | 
 45 | # Set up the logger with custom format including timestamp, level, component, and message
 46 | logging.basicConfig(
 47 |     level=logging.INFO,
 48 |     format='[%(asctime)s] [%(levelname)s] [%(component)s] %(message)s',
 49 |     handlers=[
 50 |         logging.StreamHandler(sys.stderr),
 51 |         logging.FileHandler(server_log_file)
 52 |     ]
 53 | )
 54 | 
 55 | # Add a filter to add the component field
 56 | class ComponentFilter(logging.Filter):
 57 |     def __init__(self, component="server"):
 58 |         super().__init__()
 59 |         self.component = component
 60 | 
 61 |     def filter(self, record):
 62 |         record.component = self.component
 63 |         return True
 64 | 
 65 | # Get logger and add the component filter
 66 | logger = logging.getLogger()
 67 | logger.addFilter(ComponentFilter())
 68 | 
 69 | # Add a debug file handler for detailed debugging
 70 | debug_handler = logging.FileHandler(debug_log_file)
 71 | debug_handler.setLevel(logging.DEBUG)
 72 | debug_handler.setFormatter(logging.Formatter('[%(asctime)s] [%(levelname)s] [%(component)s] %(message)s'))
 73 | logger.addHandler(debug_handler)
 74 | 
 75 | # Log basic server startup information
 76 | logger.info(f"RhinoMCP server starting in {os.getcwd()}")
 77 | logger.info(f"Log files directory: {log_dir}")
 78 | logger.info(f"Server log file: {server_log_file}")
 79 | logger.info(f"Debug log file: {debug_log_file}")
 80 | 
 81 | # Create a PID file to track this process
 82 | pid_file = os.path.join(log_dir, "combined_server.pid")
 83 | with open(pid_file, "w") as f:
 84 |     f.write(str(os.getpid()))
 85 | logger.info(f"Server PID: {os.getpid()}")
 86 | 
 87 | # Global Rhino connection
 88 | _rhino_connection = None
 89 | 
 90 | class RhinoConnection:
 91 |     """Class to manage socket connection to Rhino plugin"""
 92 |     
 93 |     def __init__(self, host: str = "localhost", port: int = 9876):
 94 |         self.host = host
 95 |         self.port = port
 96 |         self.sock = None
 97 |         self.request_id = 0
 98 |     
 99 |     def connect(self) -> bool:
100 |         """Connect to the Rhino plugin socket server"""
101 |         if self.sock:
102 |             return True
103 |             
104 |         try:
105 |             self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
106 |             self.sock.connect((self.host, self.port))
107 |             logger.info(f"Connected to Rhino at {self.host}:{self.port}")
108 |             return True
109 |         except Exception as e:
110 |             logger.error(f"Failed to connect to Rhino: {str(e)}")
111 |             logger.debug(f"Connection error details: {traceback.format_exc()}")
112 |             self.sock = None
113 |             return False
114 |     
115 |     def disconnect(self):
116 |         """Disconnect from the Rhino plugin"""
117 |         if self.sock:
118 |             try:
119 |                 self.sock.close()
120 |             except Exception as e:
121 |                 logger.error(f"Error disconnecting from Rhino: {str(e)}")
122 |             finally:
123 |                 self.sock = None
124 |     
125 |     def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
126 |         """Send a command to Rhino and return the response"""
127 |         if not self.sock and not self.connect():
128 |             raise ConnectionError("Not connected to Rhino")
129 |         
130 |         # Increment request ID for tracking
131 |         self.request_id += 1
132 |         current_request_id = self.request_id
133 |         
134 |         command = {
135 |             "id": current_request_id,
136 |             "type": command_type,
137 |             "params": params or {}
138 |         }
139 |         
140 |         try:
141 |             # Log the command being sent
142 |             logger.info(f"Request #{current_request_id}: Sending command '{command_type}' to Rhino")
143 |             logger.debug(f"Request #{current_request_id} Parameters: {json.dumps(params or {})}")
144 |             
145 |             # Send the command
146 |             command_json = json.dumps(command)
147 |             logger.debug(f"Request #{current_request_id} Raw command: {command_json}")
148 |             self.sock.sendall(command_json.encode('utf-8'))
149 |             
150 |             # Set a timeout for receiving
151 |             self.sock.settimeout(10.0)
152 |             
153 |             # Receive the response
154 |             buffer_size = 4096
155 |             response_data = b""
156 |             
157 |             while True:
158 |                 chunk = self.sock.recv(buffer_size)
159 |                 if not chunk:
160 |                     break
161 |                 response_data += chunk
162 |                 
163 |                 # Try to parse as JSON to see if we have a complete response
164 |                 try:
165 |                     json.loads(response_data.decode('utf-8'))
166 |                     # If parsing succeeds, we have a complete response
167 |                     break
168 |                 except json.JSONDecodeError:
169 |                     # Not a complete JSON yet, continue receiving
170 |                     continue
171 |             
172 |             if not response_data:
173 |                 logger.error(f"Request #{current_request_id}: No data received from Rhino")
174 |                 raise ConnectionError(f"Request #{current_request_id}: No data received from Rhino")
175 |             
176 |             # Log the raw response for debugging
177 |             raw_response = response_data.decode('utf-8')
178 |             logger.debug(f"Request #{current_request_id} Raw response: {raw_response}")
179 |             
180 |             response = json.loads(raw_response)
181 |             
182 |             # Check if the response indicates an error
183 |             if "error" in response:
184 |                 error_msg = response.get("error", "Unknown error from Rhino")
185 |                 logger.error(f"Request #{current_request_id}: Rhino reported error: {error_msg}")
186 |                 raise Exception(f"Request #{current_request_id}: Rhino error: {error_msg}")
187 |             
188 |             if response.get("status") == "error":
189 |                 error_msg = response.get("message", "Unknown error from Rhino")
190 |                 logger.error(f"Request #{current_request_id}: Rhino reported error status: {error_msg}")
191 |                 raise Exception(f"Request #{current_request_id}: Rhino error: {error_msg}")
192 |             
193 |             # Log success
194 |             logger.info(f"Request #{current_request_id}: Command '{command_type}' executed successfully")
195 |             
196 |             # If we get here, assume success and return the result
197 |             if "result" in response:
198 |                 return response.get("result", {})
199 |             else:
200 |                 # If there's no result field but no error either, return the whole response
201 |                 return response
202 |         except socket.timeout:
203 |             logger.error(f"Request #{current_request_id}: Socket timeout while waiting for response from Rhino")
204 |             logger.debug(f"Request #{current_request_id}: Timeout after 10 seconds waiting for response to '{command_type}'")
205 |             self.sock = None
206 |             raise Exception(f"Request #{current_request_id}: Timeout waiting for Rhino response")
207 |         except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
208 |             logger.error(f"Request #{current_request_id}: Socket connection error: {str(e)}")
209 |             self.sock = None
210 |             raise Exception(f"Request #{current_request_id}: Connection to Rhino lost: {str(e)}")
211 |         except json.JSONDecodeError as e:
212 |             logger.error(f"Request #{current_request_id}: Invalid JSON response: {str(e)}")
213 |             if 'response_data' in locals():
214 |                 logger.error(f"Request #{current_request_id}: Raw response causing JSON error: {response_data[:200]}")
215 |             self.sock = None
216 |             raise Exception(f"Request #{current_request_id}: Invalid JSON response from Rhino: {str(e)}")
217 |         except Exception as e:
218 |             logger.error(f"Request #{current_request_id}: Error communicating with Rhino: {str(e)}")
219 |             logger.error(f"Request #{current_request_id}: Traceback: {traceback.format_exc()}")
220 |             self.sock = None
221 |             raise Exception(f"Request #{current_request_id}: Communication error with Rhino: {str(e)}")
222 | 
223 | def get_rhino_connection() -> RhinoConnection:
224 |     """Get or create a connection to Rhino"""
225 |     global _rhino_connection
226 |     
227 |     # If we have an existing connection, check if it's still valid
228 |     if _rhino_connection is not None:
229 |         try:
230 |             # Don't use ping as it's not implemented in the Rhino plugin
231 |             # Instead, try get_scene_info which is more likely to work
232 |             _rhino_connection.send_command("get_scene_info", {})
233 |             return _rhino_connection
234 |         except Exception as e:
235 |             # Connection is dead, close it and create a new one
236 |             logger.warning(f"Existing connection is no longer valid: {str(e)}")
237 |             try:
238 |                 _rhino_connection.disconnect()
239 |             except:
240 |                 pass
241 |             _rhino_connection = None
242 |     
243 |     # Create a new connection if needed
244 |     if _rhino_connection is None:
245 |         _rhino_connection = RhinoConnection()
246 |         if not _rhino_connection.connect():
247 |             logger.error("Failed to connect to Rhino")
248 |             _rhino_connection = None
249 |             raise Exception("Could not connect to Rhino. Make sure the Rhino plugin is running.")
250 |         
251 |         # Verify connection with a known working command
252 |         try:
253 |             # Test the connection with get_scene_info command
254 |             result = _rhino_connection.send_command("get_scene_info", {})
255 |             logger.info(f"Connection test successful: {result}")
256 |         except Exception as e:
257 |             logger.error(f"Connection test failed: {str(e)}")
258 |             _rhino_connection.disconnect()
259 |             _rhino_connection = None
260 |             raise Exception(f"Rhino plugin connection test failed: {str(e)}")
261 |             
262 |         logger.info("Created new connection to Rhino")
263 |     
264 |     return _rhino_connection
265 | 
266 | # Server lifecycle management
267 | @asynccontextmanager
268 | async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
269 |     """Manage server startup and shutdown lifecycle"""
270 |     try:
271 |         logger.info("RhinoMCP server starting up")
272 |         
273 |         # Try to connect to Rhino on startup to verify it's available
274 |         try:
275 |             # This will initialize the global connection if needed
276 |             rhino = get_rhino_connection()
277 |             logger.info("Successfully connected to Rhino on startup")
278 |         except Exception as e:
279 |             logger.warning(f"Could not connect to Rhino on startup: {str(e)}")
280 |             logger.warning("Make sure the Rhino plugin is running before using Rhino resources or tools")
281 |         
282 |         yield {}  # No context resources needed for now
283 |     finally:
284 |         logger.info("RhinoMCP server shutting down")
285 |         # Cleanup code
286 |         global _rhino_connection
287 |         if _rhino_connection:
288 |             logger.info("Disconnecting from Rhino on shutdown")
289 |             _rhino_connection.disconnect()
290 |             _rhino_connection = None
291 |             
292 |         if os.path.exists(pid_file):
293 |             os.remove(pid_file)
294 |             logger.info("Removed PID file")
295 | 
296 | # Initialize the FastMCP server
297 | mcp = FastMCP(
298 |     "RhinoMcpServer",
299 |     description="A Model Context Protocol server for Rhino 3D",
300 |     lifespan=server_lifespan
301 | )
302 | 
303 | # Tool implementations using FastMCP decorators
304 | 
305 | @mcp.tool()
306 | def create_sphere(
307 |     ctx: Context,
308 |     centerX: float,
309 |     centerY: float,
310 |     centerZ: float,
311 |     radius: float,
312 |     color: Optional[str] = None
313 | ) -> str:
314 |     """
315 |     Creates a sphere with the specified center and radius.
316 |     
317 |     Parameters:
318 |     - centerX: X coordinate of the sphere center
319 |     - centerY: Y coordinate of the sphere center
320 |     - centerZ: Z coordinate of the sphere center
321 |     - radius: Radius of the sphere
322 |     - color: Optional color for the sphere (e.g., 'red', 'blue', etc.)
323 |     
324 |     Returns:
325 |     A message indicating the created sphere details
326 |     """
327 |     tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
328 |     logger.info(f"[{tool_id}] Tool call: create_sphere with center=({centerX},{centerY},{centerZ}), radius={radius}, color={color}")
329 |     
330 |     try:
331 |         # Get the Rhino connection
332 |         rhino = get_rhino_connection()
333 |         
334 |         # Send the command to Rhino
335 |         params = {
336 |             "centerX": centerX,
337 |             "centerY": centerY,
338 |             "centerZ": centerZ,
339 |             "radius": radius
340 |         }
341 |         
342 |         if color:
343 |             params["color"] = color
344 |             
345 |         result = rhino.send_command("create_sphere", params)
346 |         
347 |         # Log success
348 |         logger.info(f"[{tool_id}] Sphere created successfully")
349 |         
350 |         # Return the result
351 |         return json.dumps(result)
352 |     except Exception as e:
353 |         logger.error(f"[{tool_id}] Error creating sphere: {str(e)}")
354 |         logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
355 |         return json.dumps({
356 |             "success": False,
357 |             "error": f"Error creating sphere: {str(e)}"
358 |         })
359 | 
360 | @mcp.tool()
361 | def create_box(
362 |     ctx: Context,
363 |     cornerX: float,
364 |     cornerY: float,
365 |     cornerZ: float,
366 |     width: float,
367 |     depth: float,
368 |     height: float,
369 |     color: Optional[str] = None
370 | ) -> str:
371 |     """
372 |     Creates a box with the specified dimensions.
373 |     
374 |     Parameters:
375 |     - cornerX: X coordinate of the box corner
376 |     - cornerY: Y coordinate of the box corner
377 |     - cornerZ: Z coordinate of the box corner
378 |     - width: Width of the box (X dimension)
379 |     - depth: Depth of the box (Y dimension)
380 |     - height: Height of the box (Z dimension)
381 |     - color: Optional color for the box (e.g., 'red', 'blue', etc.)
382 |     
383 |     Returns:
384 |     A message indicating the created box details
385 |     """
386 |     tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
387 |     logger.info(f"[{tool_id}] Tool call: create_box at ({cornerX},{cornerY},{cornerZ}), size={width}x{depth}x{height}, color={color}")
388 |     
389 |     try:
390 |         # Get the Rhino connection
391 |         rhino = get_rhino_connection()
392 |         
393 |         # Send the command to Rhino
394 |         params = {
395 |             "cornerX": cornerX,
396 |             "cornerY": cornerY,
397 |             "cornerZ": cornerZ,
398 |             "width": width,
399 |             "depth": depth,
400 |             "height": height
401 |         }
402 |         
403 |         if color:
404 |             params["color"] = color
405 |             
406 |         result = rhino.send_command("create_box", params)
407 |         
408 |         # Log success
409 |         logger.info(f"[{tool_id}] Box created successfully")
410 |         
411 |         # Return the result
412 |         return json.dumps(result)
413 |     except Exception as e:
414 |         logger.error(f"[{tool_id}] Error creating box: {str(e)}")
415 |         logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
416 |         return json.dumps({
417 |             "success": False,
418 |             "error": f"Error creating box: {str(e)}",
419 |             "toolId": tool_id  # Include the tool ID for error tracking
420 |         })
421 | 
422 | @mcp.tool()
423 | def create_cylinder(
424 |     ctx: Context,
425 |     baseX: float,
426 |     baseY: float,
427 |     baseZ: float,
428 |     height: float,
429 |     radius: float,
430 |     color: Optional[str] = None
431 | ) -> str:
432 |     """
433 |     Creates a cylinder with the specified base point, height, and radius.
434 |     
435 |     Parameters:
436 |     - baseX: X coordinate of the cylinder base point
437 |     - baseY: Y coordinate of the cylinder base point
438 |     - baseZ: Z coordinate of the cylinder base point
439 |     - height: Height of the cylinder
440 |     - radius: Radius of the cylinder
441 |     - color: Optional color for the cylinder (e.g., 'red', 'blue', etc.)
442 |     
443 |     Returns:
444 |     A message indicating the created cylinder details
445 |     """
446 |     tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
447 |     logger.info(f"[{tool_id}] Tool call: create_cylinder at ({baseX},{baseY},{baseZ}), height={height}, radius={radius}, color={color}")
448 |     
449 |     try:
450 |         # Get the Rhino connection
451 |         rhino = get_rhino_connection()
452 |         
453 |         # Send the command to Rhino
454 |         params = {
455 |             "baseX": baseX,
456 |             "baseY": baseY,
457 |             "baseZ": baseZ,
458 |             "height": height,
459 |             "radius": radius
460 |         }
461 |         
462 |         if color:
463 |             params["color"] = color
464 |             
465 |         result = rhino.send_command("create_cylinder", params)
466 |         
467 |         # Log success
468 |         logger.info(f"[{tool_id}] Cylinder created successfully")
469 |         
470 |         # Return the result
471 |         return json.dumps(result)
472 |     except Exception as e:
473 |         logger.error(f"[{tool_id}] Error creating cylinder: {str(e)}")
474 |         logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
475 |         return json.dumps({
476 |             "success": False,
477 |             "error": f"Error creating cylinder: {str(e)}",
478 |             "toolId": tool_id  # Include the tool ID for error tracking
479 |         })
480 | 
481 | @mcp.tool()
482 | def get_scene_info(ctx: Context) -> str:
483 |     """
484 |     Gets information about objects in the current scene.
485 |     
486 |     Returns:
487 |     A JSON string containing scene information
488 |     """
489 |     tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
490 |     logger.info(f"[{tool_id}] Tool call: get_scene_info")
491 |     
492 |     try:
493 |         # Get the Rhino connection
494 |         rhino = get_rhino_connection()
495 |         
496 |         # Send the command to Rhino
497 |         result = rhino.send_command("get_scene_info", {})
498 |         
499 |         # Log success
500 |         logger.info(f"[{tool_id}] Scene info retrieved successfully")
501 |         
502 |         # Return the result
503 |         return json.dumps(result)
504 |     except Exception as e:
505 |         logger.error(f"[{tool_id}] Error getting scene info: {str(e)}")
506 |         logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
507 |         return json.dumps({
508 |             "success": False,
509 |             "error": f"Error getting scene info: {str(e)}",
510 |             "toolId": tool_id  # Include the tool ID for error tracking
511 |         })
512 | 
513 | @mcp.tool()
514 | def clear_scene(ctx: Context, currentLayerOnly: bool = False) -> str:
515 |     """
516 |     Clears all objects from the current scene.
517 |     
518 |     Parameters:
519 |     - currentLayerOnly: If true, only delete objects on the current layer
520 |     
521 |     Returns:
522 |     A message indicating the operation result
523 |     """
524 |     tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
525 |     layer_info = "current layer only" if currentLayerOnly else "all layers"
526 |     logger.info(f"[{tool_id}] Tool call: clear_scene ({layer_info})")
527 |     
528 |     try:
529 |         # Get the Rhino connection
530 |         rhino = get_rhino_connection()
531 |         
532 |         # Send the command to Rhino
533 |         params = {
534 |             "currentLayerOnly": currentLayerOnly
535 |         }
536 |         
537 |         result = rhino.send_command("clear_scene", params)
538 |         
539 |         # Log success
540 |         logger.info(f"[{tool_id}] Scene cleared successfully ({layer_info})")
541 |         
542 |         # Return the result
543 |         return json.dumps(result)
544 |     except Exception as e:
545 |         logger.error(f"[{tool_id}] Error clearing scene: {str(e)}")
546 |         logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
547 |         return json.dumps({
548 |             "success": False,
549 |             "error": f"Error clearing scene: {str(e)}",
550 |             "toolId": tool_id  # Include the tool ID for error tracking
551 |         })
552 | 
553 | @mcp.tool()
554 | def create_layer(ctx: Context, name: str, color: Optional[str] = None) -> str:
555 |     """
556 |     Creates a new layer in the Rhino document.
557 |     
558 |     Parameters:
559 |     - name: Name of the new layer
560 |     - color: Optional color for the layer (e.g., 'red', 'blue', etc.)
561 |     
562 |     Returns:
563 |     A message indicating the operation result
564 |     """
565 |     tool_id = str(uuid.uuid4())[:8]  # Generate a short ID for tracking this tool call
566 |     color_info = f" with color {color}" if color else ""
567 |     logger.info(f"[{tool_id}] Tool call: create_layer '{name}'{color_info}")
568 |     
569 |     try:
570 |         # Get the Rhino connection
571 |         rhino = get_rhino_connection()
572 |         
573 |         # Send the command to Rhino
574 |         params = {
575 |             "name": name
576 |         }
577 |         
578 |         if color:
579 |             params["color"] = color
580 |             
581 |         result = rhino.send_command("create_layer", params)
582 |         
583 |         # Log success
584 |         logger.info(f"[{tool_id}] Layer '{name}' created successfully")
585 |         
586 |         # Return the result
587 |         return json.dumps(result)
588 |     except Exception as e:
589 |         logger.error(f"[{tool_id}] Error creating layer: {str(e)}")
590 |         logger.error(f"[{tool_id}] Traceback: {traceback.format_exc()}")
591 |         return json.dumps({
592 |             "success": False,
593 |             "error": f"Error creating layer: {str(e)}",
594 |             "toolId": tool_id  # Include the tool ID for error tracking
595 |         })
596 | 
597 | # Record Claude interactions to debug context issues
598 | @mcp.tool()
599 | def log_claude_message(
600 |     ctx: Context,
601 |     message: str,
602 |     type: str = "info"
603 | ) -> str:
604 |     """
605 |     Log a message from Claude for debugging purposes.
606 |     
607 |     Parameters:
608 |     - message: The message to log
609 |     - type: The type of message (info, error, warning, debug)
610 |     
611 |     Returns:
612 |     Success confirmation
613 |     """
614 |     log_id = str(uuid.uuid4())[:8]
615 |     
616 |     # Create a timestamp for the filename
617 |     timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
618 |     claude_log_file = os.path.join(claude_log_dir, f"claude_{timestamp}_{log_id}.log")
619 |     
620 |     try:
621 |         with open(claude_log_file, "w") as f:
622 |             f.write(message)
623 |         
624 |         # Log to server log as well
625 |         if type == "error":
626 |             logger.error(f"[Claude] [{log_id}] {message[:100]}...")
627 |         elif type == "warning":
628 |             logger.warning(f"[Claude] [{log_id}] {message[:100]}...")
629 |         elif type == "debug":
630 |             logger.debug(f"[Claude] [{log_id}] {message[:100]}...")
631 |         else:
632 |             logger.info(f"[Claude] [{log_id}] {message[:100]}...")
633 |         
634 |         return json.dumps({
635 |             "success": True,
636 |             "logId": log_id,
637 |             "logFile": claude_log_file
638 |         })
639 |     except Exception as e:
640 |         logger.error(f"Error logging Claude message: {str(e)}")
641 |         return json.dumps({
642 |             "success": False,
643 |             "error": f"Error logging Claude message: {str(e)}"
644 |         })
645 | 
646 | def main():
647 |     """Main function to run the MCP server"""
648 |     logger.info("=== RhinoMCP Server Starting ===")
649 |     logger.info(f"Process ID: {os.getpid()}")
650 |     
651 |     try:
652 |         # Run the FastMCP server
653 |         mcp.run()
654 |     except KeyboardInterrupt:
655 |         logger.info("Keyboard interrupt received")
656 |     except Exception as e:
657 |         logger.error(f"Error running server: {str(e)}")
658 |         logger.error(f"Traceback: {traceback.format_exc()}")
659 |     finally:
660 |         logger.info("Server shutting down...")
661 |         
662 |         # Clean up connection
663 |         global _rhino_connection
664 |         if _rhino_connection:
665 |             _rhino_connection.disconnect()
666 |         
667 |         if os.path.exists(pid_file):
668 |             os.remove(pid_file)
669 |             logger.info("Removed PID file")
670 |     
671 |     return 0
672 | 
673 | if __name__ == "__main__":
674 |     sys.exit(main()) 
```

--------------------------------------------------------------------------------
/RhinoPluginFixImplementation.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using Rhino;
  2 | using Rhino.Commands;
  3 | using Rhino.PlugIns;
  4 | using System;
  5 | using System.Collections.Generic;
  6 | using System.Linq;
  7 | using System.Net;
  8 | using System.Net.Sockets;
  9 | using System.Text;
 10 | using System.Threading;
 11 | using System.Threading.Tasks;
 12 | using Newtonsoft.Json;
 13 | using Newtonsoft.Json.Linq;
 14 | using NLog;
 15 | 
 16 | namespace RhinoMcpPlugin
 17 | {
 18 |     // Main plugin class
 19 |     public class RhinoMcpPluginCommand : PlugIn
 20 |     {
 21 |         private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
 22 |         private SocketServer socketServer;
 23 |         private CommandHandlers commandHandlers;
 24 | 
 25 |         public override PlugInLoadTime LoadTime => PlugInLoadTime.AtStartup;
 26 | 
 27 |         public RhinoMcpPluginCommand()
 28 |         {
 29 |             Instance = this;
 30 |         }
 31 | 
 32 |         public static RhinoMcpPluginCommand Instance { get; private set; }
 33 | 
 34 |         protected override LoadReturnCode OnLoad(ref string errorMessage)
 35 |         {
 36 |             try
 37 |             {
 38 |                 Logger.Info("RhinoMcpPlugin loading...");
 39 |                 Logger.Info($"Rhino version: {RhinoApp.Version}");
 40 |                 Logger.Info($"Current directory: {System.IO.Directory.GetCurrentDirectory()}");
 41 | 
 42 |                 // Check document status
 43 |                 var docCount = RhinoDoc.OpenDocuments().Length;
 44 |                 Logger.Info($"Open document count: {docCount}");
 45 |                 Logger.Info($"Active document: {(RhinoDoc.ActiveDoc != null ? "exists" : "null")}");
 46 | 
 47 |                 // Create an empty document if none exists
 48 |                 if (RhinoDoc.ActiveDoc == null)
 49 |                 {
 50 |                     Logger.Info("No active document found. Creating a new document...");
 51 |                     RhinoApp.RunScript("_New", false);
 52 |                     
 53 |                     if (RhinoDoc.ActiveDoc == null)
 54 |                     {
 55 |                         Logger.Error("CRITICAL: Failed to create a new document");
 56 |                         errorMessage = "Failed to create a new Rhino document. The plugin requires an active document.";
 57 |                         return LoadReturnCode.FailedToLoad;
 58 |                     }
 59 |                     
 60 |                     Logger.Info($"New document created successfully: {RhinoDoc.ActiveDoc.Name}");
 61 |                 }
 62 | 
 63 |                 // Initialize command handlers
 64 |                 commandHandlers = new CommandHandlers();
 65 |                 
 66 |                 // Initialize socket server
 67 |                 socketServer = new SocketServer(commandHandlers);
 68 |                 var success = socketServer.Start();
 69 |                 Logger.Info($"Socket server started: {success}");
 70 |                 
 71 |                 // Register document events
 72 |                 RhinoDoc.NewDocument += OnNewDocument;
 73 |                 RhinoDoc.CloseDocument += OnCloseDocument;
 74 |                 RhinoDoc.BeginOpenDocument += OnBeginOpenDocument;
 75 |                 RhinoDoc.EndOpenDocument += OnEndOpenDocument;
 76 |                 
 77 |                 // Check all essential components
 78 |                 var componentStatus = new Dictionary<string, bool> {
 79 |                     { "SocketServer", socketServer != null },
 80 |                     { "CommandHandlers", commandHandlers != null },
 81 |                     { "RhinoDoc", RhinoDoc.ActiveDoc != null }
 82 |                 };
 83 |                 
 84 |                 foreach (var component in componentStatus)
 85 |                 {
 86 |                     Logger.Info($"Component {component.Key}: {(component.Value ? "OK" : "NULL")}");
 87 |                 }
 88 |                 
 89 |                 Logger.Info("RhinoMcpPlugin loaded successfully");
 90 |                 return LoadReturnCode.Success;
 91 |             }
 92 |             catch (Exception ex)
 93 |             {
 94 |                 Logger.Error(ex, $"Error during plugin load: {ex.Message}");
 95 |                 errorMessage = ex.Message;
 96 |                 return LoadReturnCode.FailedToLoad;
 97 |             }
 98 |         }
 99 | 
100 |         protected override void OnShutdown()
101 |         {
102 |             try
103 |             {
104 |                 Logger.Info("RhinoMcpPlugin shutting down...");
105 |                 
106 |                 // Unregister document events
107 |                 RhinoDoc.NewDocument -= OnNewDocument;
108 |                 RhinoDoc.CloseDocument -= OnCloseDocument;
109 |                 RhinoDoc.BeginOpenDocument -= OnBeginOpenDocument;
110 |                 RhinoDoc.EndOpenDocument -= OnEndOpenDocument;
111 |                 
112 |                 // Stop socket server
113 |                 if (socketServer != null)
114 |                 {
115 |                     socketServer.Stop();
116 |                     Logger.Info("Socket server stopped");
117 |                 }
118 |                 
119 |                 Logger.Info("RhinoMcpPlugin shutdown complete");
120 |             }
121 |             catch (Exception ex)
122 |             {
123 |                 Logger.Error(ex, $"Error during plugin shutdown: {ex.Message}");
124 |             }
125 |         }
126 |         
127 |         private void OnNewDocument(object sender, DocumentEventArgs e)
128 |         {
129 |             Logger.Info($"New document created: {e.Document.Name}");
130 |             Logger.Info($"Active document: {(RhinoDoc.ActiveDoc != null ? RhinoDoc.ActiveDoc.Name : "null")}");
131 |         }
132 |         
133 |         private void OnCloseDocument(object sender, DocumentEventArgs e)
134 |         {
135 |             Logger.Info($"Document closed: {e.Document.Name}");
136 |             Logger.Info($"Remaining open documents: {RhinoDoc.OpenDocuments().Length}");
137 |             
138 |             // If this was the last document, create a new one
139 |             if (RhinoDoc.OpenDocuments().Length == 0)
140 |             {
141 |                 Logger.Info("No documents remaining. Creating a new document...");
142 |                 RhinoApp.RunScript("_New", false);
143 |                 Logger.Info($"New document created: {(RhinoDoc.ActiveDoc != null ? RhinoDoc.ActiveDoc.Name : "null")}");
144 |             }
145 |         }
146 |         
147 |         private void OnBeginOpenDocument(object sender, DocumentOpenEventArgs e)
148 |         {
149 |             Logger.Info($"Beginning to open document: {e.FileName}");
150 |         }
151 |         
152 |         private void OnEndOpenDocument(object sender, DocumentOpenEventArgs e)
153 |         {
154 |             Logger.Info($"Finished opening document: {e.Document.Name}");
155 |         }
156 |     }
157 | 
158 |     // Socket server class to handle communication
159 |     public class SocketServer
160 |     {
161 |         private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
162 |         private TcpListener server;
163 |         private bool isRunning;
164 |         private CommandHandlers commandHandlers;
165 |         private CancellationTokenSource cancellationTokenSource;
166 | 
167 |         public bool IsRunning => isRunning;
168 | 
169 |         public SocketServer(CommandHandlers handlers)
170 |         {
171 |             commandHandlers = handlers;
172 |             cancellationTokenSource = new CancellationTokenSource();
173 |         }
174 | 
175 |         public bool Start(int port = 9876)
176 |         {
177 |             try
178 |             {
179 |                 Logger.Info($"Socket server initializing on port {port}");
180 |                 server = new TcpListener(IPAddress.Loopback, port);
181 |                 server.Start();
182 |                 isRunning = true;
183 | 
184 |                 // Start accepting clients in a background task
185 |                 Task.Run(() => AcceptClientsAsync(cancellationTokenSource.Token), cancellationTokenSource.Token);
186 |                 
187 |                 Logger.Info($"Socket server started successfully on port {port}");
188 |                 return true;
189 |             }
190 |             catch (Exception ex)
191 |             {
192 |                 Logger.Error(ex, $"Failed to start socket server: {ex.Message}");
193 |                 return false;
194 |             }
195 |         }
196 | 
197 |         public void Stop()
198 |         {
199 |             try
200 |             {
201 |                 Logger.Info("Stopping socket server...");
202 |                 isRunning = false;
203 |                 cancellationTokenSource.Cancel();
204 |                 server?.Stop();
205 |                 Logger.Info("Socket server stopped");
206 |             }
207 |             catch (Exception ex)
208 |             {
209 |                 Logger.Error(ex, $"Error stopping socket server: {ex.Message}");
210 |             }
211 |         }
212 | 
213 |         private async Task AcceptClientsAsync(CancellationToken cancellationToken)
214 |         {
215 |             while (isRunning && !cancellationToken.IsCancellationRequested)
216 |             {
217 |                 try
218 |                 {
219 |                     var client = await server.AcceptTcpClientAsync();
220 |                     Logger.Info($"Client connected from {client.Client.RemoteEndPoint}");
221 | 
222 |                     // Handle each client in a separate task
223 |                     _ = Task.Run(() => HandleClientAsync(client, cancellationToken), cancellationToken);
224 |                 }
225 |                 catch (OperationCanceledException)
226 |                 {
227 |                     // Normal cancellation, do nothing
228 |                     Logger.Info("Client acceptance loop cancelled");
229 |                     break;
230 |                 }
231 |                 catch (Exception ex)
232 |                 {
233 |                     Logger.Error(ex, $"Error accepting client: {ex.Message}");
234 |                     // Short delay before trying again
235 |                     await Task.Delay(1000, cancellationToken);
236 |                 }
237 |             }
238 |         }
239 | 
240 |         private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
241 |         {
242 |             using (client)
243 |             {
244 |                 try
245 |                 {
246 |                     using (var stream = client.GetStream())
247 |                     {
248 |                         var buffer = new byte[4096];
249 |                         while (isRunning && !cancellationToken.IsCancellationRequested)
250 |                         {
251 |                             // Read command from client
252 |                             var data = new List<byte>();
253 |                             int bytesRead;
254 | 
255 |                             do
256 |                             {
257 |                                 bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
258 |                                 if (bytesRead > 0)
259 |                                 {
260 |                                     data.AddRange(buffer.Take(bytesRead));
261 |                                 }
262 |                             } while (stream.DataAvailable);
263 | 
264 |                             if (bytesRead == 0)
265 |                             {
266 |                                 // Client disconnected
267 |                                 Logger.Info("Client disconnected");
268 |                                 break;
269 |                             }
270 | 
271 |                             // Process command and send response
272 |                             var commandJson = Encoding.UTF8.GetString(data.ToArray());
273 |                             Logger.Info($"Received command: {commandJson}");
274 | 
275 |                             string responseJson = ProcessCommand(commandJson);
276 |                             Logger.Debug($"Sending response: {responseJson}");
277 |                             
278 |                             var responseBytes = Encoding.UTF8.GetBytes(responseJson);
279 |                             await stream.WriteAsync(responseBytes, 0, responseBytes.Length, cancellationToken);
280 |                         }
281 |                     }
282 |                 }
283 |                 catch (OperationCanceledException)
284 |                 {
285 |                     // Normal cancellation
286 |                     Logger.Info("Client handler cancelled");
287 |                 }
288 |                 catch (Exception ex)
289 |                 {
290 |                     Logger.Error(ex, $"Error handling client: {ex.Message}");
291 |                 }
292 |                 finally
293 |                 {
294 |                     try
295 |                     {
296 |                         client.Close();
297 |                     }
298 |                     catch { /* ignore */ }
299 |                 }
300 |             }
301 |         }
302 | 
303 |         private string ProcessCommand(string commandJson)
304 |         {
305 |             try
306 |             {
307 |                 // Parse command JSON
308 |                 JObject command = JObject.Parse(commandJson);
309 |                 string commandType = command["type"]?.ToString().ToLowerInvariant();
310 |                 JObject parameters = command["params"] as JObject ?? new JObject();
311 |                 
312 |                 // Special case for health check
313 |                 if (commandType == "health_check")
314 |                 {
315 |                     return commandHandlers.HandleHealthCheck(parameters);
316 |                 }
317 |                 
318 |                 // Ensure we have an active document before processing commands
319 |                 if (RhinoDoc.ActiveDoc == null)
320 |                 {
321 |                     Logger.Error("CRITICAL: No active document available for command processing");
322 |                     return JsonConvert.SerializeObject(new
323 |                     {
324 |                         error = "No active Rhino document. Please open a document before executing commands."
325 |                     });
326 |                 }
327 |                 
328 |                 // Process regular commands
329 |                 switch (commandType)
330 |                 {
331 |                     case "ping":
332 |                         return JsonConvert.SerializeObject(new { result = "pong" });
333 |                         
334 |                     case "get_scene_info":
335 |                         return commandHandlers.HandleGetSceneInfo(parameters);
336 |                         
337 |                     case "create_box":
338 |                         return commandHandlers.HandleCreateBox(parameters);
339 |                         
340 |                     case "create_sphere":
341 |                         return commandHandlers.HandleCreateSphere(parameters);
342 |                         
343 |                     case "create_cylinder":
344 |                         return commandHandlers.HandleCreateCylinder(parameters);
345 |                         
346 |                     case "clear_scene":
347 |                         return commandHandlers.HandleClearScene(parameters);
348 |                         
349 |                     case "create_layer":
350 |                         return commandHandlers.HandleCreateLayer(parameters);
351 |                         
352 |                     default:
353 |                         Logger.Warn($"Unknown command type: {commandType}");
354 |                         return JsonConvert.SerializeObject(new { error = $"Unknown command type: {commandType}" });
355 |                 }
356 |             }
357 |             catch (NullReferenceException ex)
358 |             {
359 |                 Logger.Error(ex, $"NULL REFERENCE processing command: {commandJson}");
360 |                 Logger.Error($"Stack trace: {ex.StackTrace}");
361 |                 Logger.Error($"Context: ActiveDoc={RhinoDoc.ActiveDoc != null}");
362 |                 return JsonConvert.SerializeObject(new { error = $"Error processing command: {ex.Message}" });
363 |             }
364 |             catch (Exception ex)
365 |             {
366 |                 Logger.Error(ex, $"Error processing command: {ex.Message}");
367 |                 return JsonConvert.SerializeObject(new { error = $"Error processing command: {ex.Message}" });
368 |             }
369 |         }
370 |     }
371 | 
372 |     // Class to handle specific commands
373 |     public class CommandHandlers
374 |     {
375 |         private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
376 | 
377 |         public string HandleHealthCheck(JObject parameters)
378 |         {
379 |             try
380 |             {
381 |                 Logger.Info("Performing health check...");
382 |                 
383 |                 var healthStatus = new Dictionary<string, object> {
384 |                     { "PluginLoaded", true },
385 |                     { "RhinoVersion", RhinoApp.Version.ToString() },
386 |                     { "ActiveDocument", RhinoDoc.ActiveDoc != null },
387 |                     { "OpenDocumentCount", RhinoDoc.OpenDocuments().Length },
388 |                     { "SocketServerRunning", true },
389 |                     { "MemoryUsage", System.GC.GetTotalMemory(false) / 1024 / 1024 + " MB" },
390 |                     { "SdkVersion", typeof(RhinoApp).Assembly.GetName().Version.ToString() }
391 |                 };
392 |                 
393 |                 Logger.Info($"Health check results: {JsonConvert.SerializeObject(healthStatus)}");
394 |                 return JsonConvert.SerializeObject(new { 
395 |                     success = true, 
396 |                     result = healthStatus 
397 |                 });
398 |             }
399 |             catch (Exception ex)
400 |             {
401 |                 Logger.Error(ex, $"Exception during health check: {ex.Message}");
402 |                 return JsonConvert.SerializeObject(new { 
403 |                     error = $"Health check failed: {ex.Message}" 
404 |                 });
405 |             }
406 |         }
407 | 
408 |         public string HandleGetSceneInfo(JObject parameters)
409 |         {
410 |             Logger.Debug("Processing get_scene_info request");
411 |             try
412 |             {
413 |                 var doc = RhinoDoc.ActiveDoc;
414 |                 if (doc == null)
415 |                 {
416 |                     Logger.Error("CRITICAL: RhinoDoc.ActiveDoc is NULL");
417 |                     return JsonConvert.SerializeObject(new { 
418 |                         error = "No active Rhino document. Please open a document first." 
419 |                     });
420 |                 }
421 |                 
422 |                 Logger.Debug("Accessing document objects...");
423 |                 var objectCount = doc.Objects.Count;
424 |                 var layerCount = doc.Layers.Count;
425 |                 
426 |                 Logger.Info($"Retrieved scene info: {objectCount} objects, {layerCount} layers");
427 |                 return JsonConvert.SerializeObject(new { 
428 |                     success = true, 
429 |                     result = new {
430 |                         objectCount = objectCount,
431 |                         layerCount = layerCount,
432 |                         documentName = doc.Name,
433 |                         activeLayer = doc.Layers.CurrentLayer.Name
434 |                     }
435 |                 });
436 |             }
437 |             catch (NullReferenceException ex)
438 |             {
439 |                 Logger.Error(ex, $"NULL REFERENCE in get_scene_info: {ex.Message}");
440 |                 Logger.Error($"Stack trace: {ex.StackTrace}");
441 |                 return JsonConvert.SerializeObject(new { 
442 |                     error = $"Error processing command: {ex.Message}" 
443 |                 });
444 |             }
445 |             catch (Exception ex)
446 |             {
447 |                 Logger.Error(ex, $"Exception in get_scene_info: {ex.Message}");
448 |                 return JsonConvert.SerializeObject(new { 
449 |                     error = $"Error getting scene info: {ex.Message}" 
450 |                 });
451 |             }
452 |         }
453 | 
454 |         public string HandleCreateBox(JObject parameters)
455 |         {
456 |             Logger.Debug($"Processing create_box with parameters: {parameters}");
457 |             
458 |             try
459 |             {
460 |                 // Log parameter extraction
461 |                 Logger.Debug("Extracting parameters...");
462 |                 double cornerX = parameters.Value<double>("cornerX");
463 |                 double cornerY = parameters.Value<double>("cornerY");
464 |                 double cornerZ = parameters.Value<double>("cornerZ");
465 |                 double width = parameters.Value<double>("width");
466 |                 double depth = parameters.Value<double>("depth");
467 |                 double height = parameters.Value<double>("height");
468 |                 string color = parameters.Value<string>("color");
469 |                 
470 |                 // Log document access
471 |                 Logger.Debug("Accessing Rhino document...");
472 |                 var doc = RhinoDoc.ActiveDoc;
473 |                 if (doc == null)
474 |                 {
475 |                     Logger.Error("CRITICAL: RhinoDoc.ActiveDoc is NULL");
476 |                     return JsonConvert.SerializeObject(new { 
477 |                         error = "No active Rhino document. Please open a document first." 
478 |                     });
479 |                 }
480 |                 
481 |                 // Run on main UI thread
482 |                 bool success = false;
483 |                 object result = null;
484 |                 
485 |                 // Execute on the main Rhino UI thread
486 |                 RhinoApp.InvokeOnUiThread(new Action(() =>
487 |                 {
488 |                     try
489 |                     {
490 |                         // Log geometric operations with safeguards
491 |                         Logger.Debug("Creating geometry...");
492 |                         var corner = new Rhino.Geometry.Point3d(cornerX, cornerY, cornerZ);
493 |                         var box = new Rhino.Geometry.Box(
494 |                             new Rhino.Geometry.Plane(corner, Rhino.Geometry.Vector3d.ZAxis),
495 |                             new Rhino.Geometry.Interval(0, width),
496 |                             new Rhino.Geometry.Interval(0, depth),
497 |                             new Rhino.Geometry.Interval(0, height)
498 |                         );
499 |                         
500 |                         // Verify box was created
501 |                         if (box == null || !box.IsValid)
502 |                         {
503 |                             Logger.Error($"Box creation failed: {(box == null ? "null box" : "invalid box")}");
504 |                             result = new { error = "Failed to create valid box geometry" };
505 |                             return;
506 |                         }
507 |                         
508 |                         // Log document modification
509 |                         Logger.Debug("Adding to document...");
510 |                         var id = doc.Objects.AddBox(box);
511 |                         if (id == Guid.Empty)
512 |                         {
513 |                             Logger.Error("Failed to add box to document");
514 |                             result = new { error = "Failed to add box to document" };
515 |                             return;
516 |                         }
517 |                         
518 |                         // Apply color if specified
519 |                         if (!string.IsNullOrEmpty(color))
520 |                         {
521 |                             System.Drawing.Color objColor = System.Drawing.Color.FromName(color);
522 |                             if (objColor.A > 0)
523 |                             {
524 |                                 var objAttributes = new Rhino.DocObjects.ObjectAttributes();
525 |                                 objAttributes.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject;
526 |                                 objAttributes.ObjectColor = objColor;
527 |                                 doc.Objects.ModifyAttributes(id, objAttributes, true);
528 |                             }
529 |                         }
530 |                         
531 |                         // Update views
532 |                         doc.Views.Redraw();
533 |                         
534 |                         // Log successful operation
535 |                         Logger.Info($"Successfully created box with ID {id}");
536 |                         result = new { success = true, objectId = id.ToString() };
537 |                         success = true;
538 |                     }
539 |                     catch (Exception ex)
540 |                     {
541 |                         Logger.Error(ex, $"Error in UI thread: {ex.Message}");
542 |                         result = new { error = $"Error in UI thread: {ex.Message}" };
543 |                     }
544 |                 }));
545 |                 
546 |                 if (success)
547 |                 {
548 |                     return JsonConvert.SerializeObject(result);
549 |                 }
550 |                 else
551 |                 {
552 |                     return JsonConvert.SerializeObject(result ?? new { error = "Unknown error creating box" });
553 |                 }
554 |             }
555 |             catch (NullReferenceException ex)
556 |             {
557 |                 Logger.Error(ex, $"NULL REFERENCE in create_box: {ex.Message}");
558 |                 Logger.Error($"Stack trace: {ex.StackTrace}");
559 |                 return JsonConvert.SerializeObject(new { 
560 |                     error = $"Error processing command: {ex.Message}" 
561 |                 });
562 |             }
563 |             catch (Exception ex)
564 |             {
565 |                 Logger.Error(ex, $"Exception in create_box: {ex.Message}");
566 |                 return JsonConvert.SerializeObject(new { 
567 |                     error = $"Error processing command: {ex.Message}" 
568 |                 });
569 |             }
570 |         }
571 | 
572 |         public string HandleCreateSphere(JObject parameters)
573 |         {
574 |             Logger.Debug($"Processing create_sphere with parameters: {parameters}");
575 |             
576 |             try
577 |             {
578 |                 // Extract parameters
579 |                 Logger.Debug("Extracting parameters...");
580 |                 double centerX = parameters.Value<double>("centerX");
581 |                 double centerY = parameters.Value<double>("centerY");
582 |                 double centerZ = parameters.Value<double>("centerZ");
583 |                 double radius = parameters.Value<double>("radius");
584 |                 string color = parameters.Value<string>("color");
585 |                 
586 |                 // Access document
587 |                 Logger.Debug("Accessing Rhino document...");
588 |                 var doc = RhinoDoc.ActiveDoc;
589 |                 if (doc == null)
590 |                 {
591 |                     Logger.Error("CRITICAL: RhinoDoc.ActiveDoc is NULL");
592 |                     return JsonConvert.SerializeObject(new { 
593 |                         error = "No active Rhino document. Please open a document first." 
594 |                     });
595 |                 }
596 |                 
597 |                 // Execute on UI thread
598 |                 bool success = false;
599 |                 object result = null;
600 |                 
601 |                 RhinoApp.InvokeOnUiThread(new Action(() =>
602 |                 {
603 |                     try
604 |                     {
605 |                         // Create geometry
606 |                         Logger.Debug("Creating sphere geometry...");
607 |                         var center = new Rhino.Geometry.Point3d(centerX, centerY, centerZ);
608 |                         var sphere = new Rhino.Geometry.Sphere(center, radius);
609 |                         
610 |                         if (!sphere.IsValid)
611 |                         {
612 |                             Logger.Error("Invalid sphere geometry");
613 |                             result = new { error = "Failed to create valid sphere geometry" };
614 |                             return;
615 |                         }
616 |                         
617 |                         // Add to document
618 |                         Logger.Debug("Adding sphere to document...");
619 |                         var id = doc.Objects.AddSphere(sphere);
620 |                         if (id == Guid.Empty)
621 |                         {
622 |                             Logger.Error("Failed to add sphere to document");
623 |                             result = new { error = "Failed to add sphere to document" };
624 |                             return;
625 |                         }
626 |                         
627 |                         // Apply color if specified
628 |                         if (!string.IsNullOrEmpty(color))
629 |                         {
630 |                             System.Drawing.Color objColor = System.Drawing.Color.FromName(color);
631 |                             if (objColor.A > 0)
632 |                             {
633 |                                 var objAttributes = new Rhino.DocObjects.ObjectAttributes();
634 |                                 objAttributes.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject;
635 |                                 objAttributes.ObjectColor = objColor;
636 |                                 doc.Objects.ModifyAttributes(id, objAttributes, true);
637 |                             }
638 |                         }
639 |                         
640 |                         // Update views
641 |                         doc.Views.Redraw();
642 |                         
643 |                         Logger.Info($"Successfully created sphere with ID {id}");
644 |                         result = new { success = true, objectId = id.ToString() };
645 |                         success = true;
646 |                     }
647 |                     catch (Exception ex)
648 |                     {
649 |                         Logger.Error(ex, $"Error in UI thread: {ex.Message}");
650 |                         result = new { error = $"Error in UI thread: {ex.Message}" };
651 |                     }
652 |                 }));
653 |                 
654 |                 if (success)
655 |                 {
656 |                     return JsonConvert.SerializeObject(result);
657 |                 }
658 |                 else
659 |                 {
660 |                     return JsonConvert.SerializeObject(result ?? new { error = "Unknown error creating sphere" });
661 |                 }
662 |             }
663 |             catch (Exception ex)
664 |             {
665 |                 Logger.Error(ex, $"Exception in create_sphere: {ex.Message}");
666 |                 return JsonConvert.SerializeObject(new { 
667 |                     error = $"Error creating sphere: {ex.Message}" 
668 |                 });
669 |             }
670 |         }
671 | 
672 |         public string HandleCreateCylinder(JObject parameters)
673 |         {
674 |             Logger.Debug($"Processing create_cylinder with parameters: {parameters}");
675 |             
676 |             try
677 |             {
678 |                 // Extract parameters
679 |                 double baseX = parameters.Value<double>("baseX");
680 |                 double baseY = parameters.Value<double>("baseY");
681 |                 double baseZ = parameters.Value<double>("baseZ");
682 |                 double height = parameters.Value<double>("height");
683 |                 double radius = parameters.Value<double>("radius");
684 |                 string color = parameters.Value<string>("color");
685 |                 
686 |                 // Access document
687 |                 var doc = RhinoDoc.ActiveDoc;
688 |                 if (doc == null)
689 |                 {
690 |                     Logger.Error("CRITICAL: RhinoDoc.ActiveDoc is NULL");
691 |                     return JsonConvert.SerializeObject(new { 
692 |                         error = "No active Rhino document. Please open a document first." 
693 |                     });
694 |                 }
695 |                 
696 |                 // Execute on UI thread
697 |                 bool success = false;
698 |                 object result = null;
699 |                 
700 |                 RhinoApp.InvokeOnUiThread(new Action(() =>
701 |                 {
702 |                     try
703 |                     {
704 |                         // Create geometry
705 |                         var basePt = new Rhino.Geometry.Point3d(baseX, baseY, baseZ);
706 |                         var topPt = new Rhino.Geometry.Point3d(baseX, baseY, baseZ + height);
707 |                         var cylinder = new Rhino.Geometry.Cylinder(
708 |                             new Rhino.Geometry.Circle(basePt, radius),
709 |                             height
710 |                         );
711 |                         
712 |                         if (!cylinder.IsValid)
713 |                         {
714 |                             Logger.Error("Invalid cylinder geometry");
715 |                             result = new { error = "Failed to create valid cylinder geometry" };
716 |                             return;
717 |                         }
718 |                         
719 |                         // Add to document
720 |                         var id = doc.Objects.AddCylinder(cylinder);
721 |                         if (id == Guid.Empty)
722 |                         {
723 |                             Logger.Error("Failed to add cylinder to document");
724 |                             result = new { error = "Failed to add cylinder to document" };
725 |                             return;
726 |                         }
727 |                         
728 |                         // Apply color if specified
729 |                         if (!string.IsNullOrEmpty(color))
730 |                         {
731 |                             System.Drawing.Color objColor = System.Drawing.Color.FromName(color);
732 |                             if (objColor.A > 0)
733 |                             {
734 |                                 var objAttributes = new Rhino.DocObjects.ObjectAttributes();
735 |                                 objAttributes.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject;
736 |                                 objAttributes.ObjectColor = objColor;
737 |                                 doc.Objects.ModifyAttributes(id, objAttributes, true);
738 |                             }
739 |                         }
740 |                         
741 |                         // Update views
742 |                         doc.Views.Redraw();
743 |                         
744 |                         Logger.Info($"Successfully created cylinder with ID {id}");
745 |                         result = new { success = true, objectId = id.ToString() };
746 |                         success = true;
747 |                     }
748 |                     catch (Exception ex)
749 |                     {
750 |                         Logger.Error(ex, $"Error in UI thread: {ex.Message}");
751 |                         result = new { error = $"Error in UI thread: {ex.Message}" };
752 |                     }
753 |                 }));
754 |                 
755 |                 if (success)
756 |                 {
757 |                     return JsonConvert.SerializeObject(result);
758 |                 }
759 |                 else
760 |                 {
761 |                     return JsonConvert.SerializeObject(result ?? new { error = "Unknown error creating cylinder" });
762 |                 }
763 |             }
764 |             catch (Exception ex)
765 |             {
766 |                 Logger.Error(ex, $"Exception in create_cylinder: {ex.Message}");
767 |                 return JsonConvert.SerializeObject(new { 
768 |                     error = $"Error creating cylinder: {ex.Message}" 
769 |                 });
770 |             }
771 |         }
772 | 
773 |         public string HandleClearScene(JObject parameters)
774 |         {
775 |             Logger.Debug($"Processing clear_scene with parameters: {parameters}");
776 |             
777 |             try
778 |             {
779 |                 bool currentLayerOnly = parameters.Value<bool>("currentLayerOnly");
780 |                 
781 |                 // Access document
782 |                 var doc = RhinoDoc.ActiveDoc;
783 |                 if (doc == null)
784 |                 {
785 |                     Logger.Error("CRITICAL: RhinoDoc.ActiveDoc is NULL");
786 |                     return JsonConvert.SerializeObject(new { 
787 |                         error = "No active Rhino document. Please open a document first." 
788 |                     });
789 |                 }
790 |                 
791 |                 // Execute on UI thread
792 |                 bool success = false;
793 |                 object result = null;
794 |                 
795 |                 RhinoApp.InvokeOnUiThread(new Action(() =>
796 |                 {
797 |                     try
798 |                     {
799 |                         int deletedCount = 0;
800 |                         
801 |                         if (currentLayerOnly)
802 |                         {
803 |                             // Get current layer index
804 |                             int currentLayerIndex = doc.Layers.CurrentLayerIndex;
805 |                             
806 |                             // Delete objects on current layer
807 |                             var objectsToDelete = new List<Guid>();
808 |                             foreach (var rhObj in doc.Objects)
809 |                             {
810 |                                 if (rhObj.Attributes.LayerIndex == currentLayerIndex)
811 |                                 {
812 |                                     objectsToDelete.Add(rhObj.Id);
813 |                                     deletedCount++;
814 |                                 }
815 |                             }
816 |                             
817 |                             foreach (var id in objectsToDelete)
818 |                             {
819 |                                 doc.Objects.Delete(id, true);
820 |                             }
821 |                             
822 |                             Logger.Info($"Deleted {deletedCount} objects from current layer");
823 |                         }
824 |                         else
825 |                         {
826 |                             // Delete all objects
827 |                             deletedCount = doc.Objects.Count;
828 |                             doc.Objects.Clear();
829 |                             Logger.Info($"Cleared all {deletedCount} objects from document");
830 |                         }
831 |                         
832 |                         // Update views
833 |                         doc.Views.Redraw();
834 |                         
835 |                         result = new { 
836 |                             success = true, 
837 |                             deletedCount = deletedCount,
838 |                             currentLayerOnly = currentLayerOnly
839 |                         };
840 |                         success = true;
841 |                     }
842 |                     catch (Exception ex)
843 |                     {
844 |                         Logger.Error(ex, $"Error in UI thread: {ex.Message}");
845 |                         result = new { error = $"Error in UI thread: {ex.Message}" };
846 |                     }
847 |                 }));
848 |                 
849 |                 if (success)
850 |                 {
851 |                     return JsonConvert.SerializeObject(result);
852 |                 }
853 |                 else
854 |                 {
855 |                     return JsonConvert.SerializeObject(result ?? new { error = "Unknown error clearing scene" });
856 |                 }
857 |             }
858 |             catch (Exception ex)
859 |             {
860 |                 Logger.Error(ex, $"Exception in clear_scene: {ex.Message}");
861 |                 return JsonConvert.SerializeObject(new { 
862 |                     error = $"Error clearing scene: {ex.Message}" 
863 |                 });
864 |             }
865 |         }
866 | 
867 |         public string HandleCreateLayer(JObject parameters)
868 |         {
869 |             Logger.Debug($"Processing create_layer with parameters: {parameters}");
870 |             
871 |             try
872 |             {
873 |                 string name = parameters.Value<string>("name");
874 |                 string color = parameters.Value<string>("color");
875 |                 
876 |                 if (string.IsNullOrEmpty(name))
877 |                 {
878 |                     Logger.Error("Layer name is required");
879 |                     return JsonConvert.SerializeObject(new { 
880 |                         error = "Layer name is required" 
881 |                     });
882 |                 }
883 |                 
884 |                 // Access document
885 |                 var doc = RhinoDoc.ActiveDoc;
886 |                 if (doc == null)
887 |                 {
888 |                     Logger.Error("CRITICAL: RhinoDoc.ActiveDoc is NULL");
889 |                     return JsonConvert.SerializeObject(new { 
890 |                         error = "No active Rhino document. Please open a document first." 
891 |                     });
892 |                 }
893 |                 
894 |                 // Execute on UI thread
895 |                 bool success = false;
896 |                 object result = null;
897 |                 
898 |                 RhinoApp.InvokeOnUiThread(new Action(() =>
899 |                 {
900 |                     try
901 |                     {
902 |                         // Check if layer already exists
903 |                         int existingIndex = doc.Layers.Find(name, true);
904 |                         if (existingIndex >= 0)
905 |                         {
906 |                             Logger.Warning($"Layer '{name}' already exists");
907 |                             var layer = doc.Layers[existingIndex];
908 |                             result = new { 
909 |                                 success = true, 
910 |                                 layerId = layer.Id.ToString(),
911 |                                 layerName = layer.Name,
912 |                                 alreadyExisted = true
913 |                             };
914 |                             doc.Layers.SetCurrentLayerIndex(existingIndex, true);
915 |                             success = true;
916 |                             return;
917 |                         }
918 |                         
919 |                         // Create new layer
920 |                         var newLayer = new Rhino.DocObjects.Layer();
921 |                         newLayer.Name = name;
922 |                         
923 |                         // Set color if specified
924 |                         if (!string.IsNullOrEmpty(color))
925 |                         {
926 |                             System.Drawing.Color layerColor = System.Drawing.Color.FromName(color);
927 |                             if (layerColor.A > 0)
928 |                             {
929 |                                 newLayer.Color = layerColor;
930 |                             }
931 |                         }
932 |                         
933 |                         // Add layer to document
934 |                         int index = doc.Layers.Add(newLayer);
935 |                         if (index < 0)
936 |                         {
937 |                             Logger.Error($"Failed to add layer '{name}' to document");
938 |                             result = new { error = $"Failed to add layer '{name}' to document" };
939 |                             return;
940 |                         }
941 |                         
942 |                         // Set as current layer
943 |                         doc.Layers.SetCurrentLayerIndex(index, true);
944 |                         
945 |                         Logger.Info($"Successfully created layer '{name}' with index {index}");
946 |                         result = new { 
947 |                             success = true, 
948 |                             layerId = newLayer.Id.ToString(),
949 |                             layerName = newLayer.Name,
950 |                             layerIndex = index
951 |                         };
952 |                         success = true;
953 |                     }
954 |                     catch (Exception ex)
955 |                     {
956 |                         Logger.Error(ex, $"Error in UI thread: {ex.Message}");
957 |                         result = new { error = $"Error in UI thread: {ex.Message}" };
958 |                     }
959 |                 }));
960 |                 
961 |                 if (success)
962 |                 {
963 |                     return JsonConvert.SerializeObject(result);
964 |                 }
965 |                 else
966 |                 {
967 |                     return JsonConvert.SerializeObject(result ?? new { error = $"Unknown error creating layer '{name}'" });
968 |                 }
969 |             }
970 |             catch (Exception ex)
971 |             {
972 |                 Logger.Error(ex, $"Exception in create_layer: {ex.Message}");
973 |                 return JsonConvert.SerializeObject(new { 
974 |                     error = $"Error creating layer: {ex.Message}" 
975 |                 });
976 |             }
977 |         }
978 |     }
979 | } 
```
Page 2/3FirstPrevNextLast