diff --git a/MCP_README.md b/MCP_README.md new file mode 100644 index 0000000..c36a2e8 --- /dev/null +++ b/MCP_README.md @@ -0,0 +1,60 @@ +# LSL Development MCP Server + +This MCP (Model Context Protocol) server provides AI assistants with direct access to your LSL development tools and Peril Dice project. + +## What it provides + +### Available Tools: +1. **lint_lsl_file** - Lint/validate LSL syntax for a specific file using lslint +2. **lint_all_lsl** - Lint/validate all LSL files in the project using lslint +3. **list_lsl_files** - List all LSL files in the project (currently shows 20 files) +4. **git_status** - Get git status of the project +5. **analyze_script_architecture** - Analyze script architecture and communication patterns +6. **get_memory_usage_info** - Extract memory usage information from LSL files + +### Available Resources: +- Project README.md +- CHANGELOG.md +- WARP.md (development guide) + +## Files Created: +- `lsl_mcp_server.py` - The main MCP server +- `mcp_config.json` - Configuration for MCP clients +- `test_mcp_server.py` - Test script to verify server functionality + +## How to Use: + +### Testing the Server: +```bash +python3 test_mcp_server.py +``` + +### Manual Server Start: +```bash +python3 lsl_mcp_server.py +``` + +### Integration with AI Tools: +The `mcp_config.json` file can be used to configure MCP-compatible AI tools to connect to this server. + +## Benefits: + +With this MCP server, AI assistants can: +- Directly lint and validate your LSL code using lslint +- List and examine all 20 LSL files in your Peril project +- Check git status and project state +- Analyze your script architecture (Controllers, Handlers, Modules, Linkset scripts) +- Access project documentation +- Monitor memory usage information from script comments + +This creates a much more interactive and helpful development experience, where the AI can actually run your development tools and provide real-time feedback on your Peril Dice project. + +## Security: + +The server only has access to: +- Your Peril project directory (`/home/richard/peril`) +- Your lslint tool (`/home/richard/lslint/lslint`) +- Git commands within the project directory +- Read-only access to project documentation + +It cannot modify files outside the project scope or execute arbitrary system commands. \ No newline at end of file diff --git a/debug_mcp.py b/debug_mcp.py new file mode 100644 index 0000000..e4e0f01 --- /dev/null +++ b/debug_mcp.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import sys +import json +import traceback + +def debug_main(): + """Debug version to see what's happening""" + print('{"debug": "server started"}', flush=True) + + try: + while True: + line = sys.stdin.readline() + print(f'{{"debug": "received line: {line.strip()[:100]}"}}', flush=True) + + if not line: + print('{"debug": "EOF received, exiting"}', flush=True) + break + + line = line.strip() + if not line: + continue + + try: + request = json.loads(line) + print(f'{{"debug": "parsed request: {request.get("method", "unknown")}"}}', flush=True) + + if request.get("method") == "initialize": + response = { + "jsonrpc": "2.0", + "id": request.get("id"), + "result": { + "protocolVersion": "2025-03-26", + "capabilities": {"tools": {}, "resources": {}}, + "serverInfo": {"name": "Debug LSL Server", "version": "1.0.0"} + } + } + print(json.dumps(response), flush=True) + print('{"debug": "sent initialize response"}', flush=True) + + except Exception as e: + print(f'{{"debug": "error parsing: {str(e)}"}}', flush=True) + + except Exception as e: + print(f'{{"debug": "main loop error: {str(e)}"}}', flush=True) + traceback.print_exc(file=sys.stderr) + + print('{"debug": "server exiting"}', flush=True) + +if __name__ == "__main__": + debug_main() \ No newline at end of file diff --git a/lsl_mcp_server.py b/lsl_mcp_server.py new file mode 100755 index 0000000..bbed2e9 --- /dev/null +++ b/lsl_mcp_server.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +""" +LSL Development MCP Server for Peril Dice Project +Provides tools for LSL validation, preprocessing, and project management. +""" + +import json +import sys +import subprocess +import os +import glob +from pathlib import Path +from typing import Dict, List, Any, Optional + + +class LSLMCPServer: + def __init__(self, project_root: str = "/home/richard/peril"): + self.project_root = Path(project_root) + self.lsl_validator_path = self.project_root / "lsl_validator.py" + + def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]: + """Handle incoming MCP requests""" + method = request.get("method", "") + params = request.get("params", {}) + + if method == "initialize": + return self._handle_initialize(params) + elif method == "tools/list": + return self._list_tools() + elif method == "tools/call": + return self._call_tool(params) + elif method == "resources/list": + return self._list_resources() + elif method == "resources/read": + return self._read_resource(params) + else: + return {"error": f"Unknown method: {method}"} + + def _handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Handle MCP initialization request""" + # Use the client's protocol version if provided + client_version = params.get("protocolVersion", "2024-11-05") + return { + "protocolVersion": client_version, + "capabilities": { + "tools": {}, + "resources": {} + }, + "serverInfo": { + "name": "LSL Development Server", + "version": "1.0.0" + } + } + + def _list_tools(self) -> Dict[str, Any]: + """List available LSL development tools""" + return { + "tools": [ + { + "name": "validate_lsl_file", + "description": "Comprehensive LSL validation for a specific file (syntax, scope, style, performance, memory)", + "inputSchema": { + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "Path to LSL file to validate"} + }, + "required": ["file_path"] + } + }, + { + "name": "validate_all_lsl", + "description": "Comprehensive LSL validation for all files in the project", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "list_lsl_files", + "description": "List all LSL files in the project", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "git_status", + "description": "Get git status of the project", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "analyze_script_architecture", + "description": "Analyze the script architecture and communication patterns", + "inputSchema": { + "type": "object", + "properties": {} + } + }, + { + "name": "get_memory_usage_info", + "description": "Extract memory usage information from LSL files", + "inputSchema": { + "type": "object", + "properties": {} + } + } + ] + } + + def _call_tool(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Execute a specific tool""" + tool_name = params.get("name", "") + arguments = params.get("arguments", {}) + + try: + if tool_name == "validate_lsl_file": + return self._validate_lsl_file(arguments["file_path"]) + elif tool_name == "validate_all_lsl": + return self._validate_all_lsl() + elif tool_name == "list_lsl_files": + return self._list_lsl_files() + elif tool_name == "git_status": + return self._git_status() + elif tool_name == "analyze_script_architecture": + return self._analyze_script_architecture() + elif tool_name == "get_memory_usage_info": + return self._get_memory_usage_info() + else: + return {"error": f"Unknown tool: {tool_name}"} + except Exception as e: + return {"error": f"Tool execution failed: {str(e)}"} + + def _validate_lsl_file(self, file_path: str) -> Dict[str, Any]: + """Validate a specific LSL file using the comprehensive LSL validator""" + full_path = self.project_root / file_path if not os.path.isabs(file_path) else Path(file_path) + + if not full_path.exists(): + return {"error": f"File not found: {full_path}"} + + if not self.lsl_validator_path.exists(): + return {"error": f"LSL validator not found at: {self.lsl_validator_path}"} + + try: + result = subprocess.run([ + "python3", str(self.lsl_validator_path), str(full_path) + ], capture_output=True, text=True, cwd=str(self.project_root)) + + return { + "content": [ + { + "type": "text", + "text": f"LSL Validation Result for {file_path}:\n\n" + f"Exit Code: {result.returncode}\n" + f"Output:\n{result.stdout}\n" + f"Errors:\n{result.stderr}" + } + ] + } + except Exception as e: + return {"error": f"Failed to run LSL validator: {str(e)}"} + + def _validate_all_lsl(self) -> Dict[str, Any]: + """Validate all LSL files in the project using the comprehensive LSL validator""" + if not self.lsl_validator_path.exists(): + return {"error": f"LSL validator not found at: {self.lsl_validator_path}"} + + try: + result = subprocess.run([ + "python3", str(self.lsl_validator_path), "." + ], capture_output=True, text=True, cwd=str(self.project_root)) + + return { + "content": [ + { + "type": "text", + "text": f"LSL Validation Results for All Files:\n\n" + f"Exit Code: {result.returncode}\n\n" + f"Output:\n{result.stdout}\n" + f"Errors:\n{result.stderr}" + } + ] + } + except Exception as e: + return {"error": f"Failed to run LSL validator on all files: {str(e)}"} + + def _list_lsl_files(self) -> Dict[str, Any]: + """List all LSL files in the project""" + lsl_files = list(self.project_root.glob("*.lsl")) + file_list = [f.name for f in sorted(lsl_files)] + + return { + "content": [ + { + "type": "text", + "text": f"LSL Files in Project ({len(file_list)} total):\n\n" + + "\n".join([f"• {f}" for f in file_list]) + } + ] + } + + def _git_status(self) -> Dict[str, Any]: + """Get git status of the project""" + try: + result = subprocess.run([ + "git", "status", "--porcelain" + ], capture_output=True, text=True, cwd=str(self.project_root)) + + branch_result = subprocess.run([ + "git", "branch", "--show-current" + ], capture_output=True, text=True, cwd=str(self.project_root)) + + status_text = "Git Status:\n\n" + status_text += f"Current Branch: {branch_result.stdout.strip()}\n\n" + + if result.stdout.strip(): + status_text += "Modified Files:\n" + for line in result.stdout.strip().split('\n'): + status_text += f" {line}\n" + else: + status_text += "Working directory clean\n" + + return { + "content": [ + { + "type": "text", + "text": status_text + } + ] + } + except Exception as e: + return {"error": f"Failed to get git status: {str(e)}"} + + def _analyze_script_architecture(self) -> Dict[str, Any]: + """Analyze the script architecture""" + lsl_files = list(self.project_root.glob("*.lsl")) + + architecture_info = "Peril Dice Script Architecture Analysis:\n\n" + architecture_info += f"Total Scripts: {len(lsl_files)}\n\n" + + # Categorize scripts by type + controllers = [f for f in lsl_files if "Controller" in f.name or "Manager" in f.name] + handlers = [f for f in lsl_files if "Handler" in f.name] + modules = [f for f in lsl_files if "Module" in f.name] + linkset_scripts = [f for f in lsl_files if "Linkset" in f.name] + + architecture_info += f"Controllers/Managers ({len(controllers)}):\n" + for script in controllers: + architecture_info += f" • {script.name}\n" + + architecture_info += f"\nHandlers ({len(handlers)}):\n" + for script in handlers: + architecture_info += f" • {script.name}\n" + + architecture_info += f"\nModules ({len(modules)}):\n" + for script in modules: + architecture_info += f" • {script.name}\n" + + architecture_info += f"\nLinkset-Specific Scripts ({len(linkset_scripts)}):\n" + for script in linkset_scripts: + architecture_info += f" • {script.name}\n" + + return { + "content": [ + { + "type": "text", + "text": architecture_info + } + ] + } + + def _get_memory_usage_info(self) -> Dict[str, Any]: + """Extract memory usage information from comments in LSL files""" + lsl_files = list(self.project_root.glob("*.lsl")) + memory_info = "Memory Usage Information:\n\n" + + for lsl_file in lsl_files: + try: + with open(lsl_file, 'r', encoding='utf-8') as f: + content = f.read() + # Look for memory usage comments + lines = content.split('\n') + memory_lines = [line for line in lines if 'memory' in line.lower() and ('%' in line or 'usage' in line.lower())] + + if memory_lines: + memory_info += f"{lsl_file.name}:\n" + for line in memory_lines: + memory_info += f" {line.strip()}\n" + memory_info += "\n" + except Exception: + continue + + return { + "content": [ + { + "type": "text", + "text": memory_info if memory_info.strip() != "Memory Usage Information:" else "No memory usage information found in comments." + } + ] + } + + def _list_resources(self) -> Dict[str, Any]: + """List available project resources""" + return { + "resources": [ + { + "uri": "file:///home/richard/peril/README.md", + "name": "Project README", + "description": "Main project documentation", + "mimeType": "text/markdown" + }, + { + "uri": "file:///home/richard/peril/CHANGELOG.md", + "name": "Changelog", + "description": "Project changelog", + "mimeType": "text/markdown" + }, + { + "uri": "file:///home/richard/peril/WARP.md", + "name": "WARP Development Guide", + "description": "Development guidance for WARP", + "mimeType": "text/markdown" + } + ] + } + + def _read_resource(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Read a specific resource""" + uri = params.get("uri", "") + + if uri.startswith("file://"): + file_path = Path(uri[7:]) # Remove file:// prefix + + if not file_path.exists(): + return {"error": f"File not found: {file_path}"} + + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + return { + "contents": [ + { + "uri": uri, + "mimeType": "text/markdown" if file_path.suffix == ".md" else "text/plain", + "text": content + } + ] + } + except Exception as e: + return {"error": f"Failed to read file: {str(e)}"} + + return {"error": f"Unsupported URI: {uri}"} + + +def main(): + """Main server loop""" + server = LSLMCPServer() + + try: + # Read requests from stdin continuously + while True: + try: + line = sys.stdin.readline() + if not line: # EOF + break + + line = line.strip() + if not line: + continue + + request = json.loads(line) + response = server.handle_request(request) + + # Wrap response in proper JSON-RPC format + json_rpc_response = { + "jsonrpc": "2.0", + "id": request.get("id", None) + } + + if "error" in response: + json_rpc_response["error"] = response["error"] + else: + json_rpc_response["result"] = response + + print(json.dumps(json_rpc_response)) + sys.stdout.flush() # Ensure immediate output + + except json.JSONDecodeError: + error_response = { + "jsonrpc": "2.0", + "error": {"code": -32700, "message": "Parse error"}, + "id": None + } + print(json.dumps(error_response)) + sys.stdout.flush() + except Exception as e: + error_response = { + "jsonrpc": "2.0", + "error": {"code": -32603, "message": str(e)}, + "id": None + } + print(json.dumps(error_response)) + sys.stdout.flush() + except KeyboardInterrupt: + pass + except EOFError: + pass + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/mcp_config.json b/mcp_config.json new file mode 100644 index 0000000..07d5797 --- /dev/null +++ b/mcp_config.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "lsl-dev": { + "command": "python3", + "args": ["/home/richard/peril/lsl_mcp_server.py"], + "env": { + "PYTHONPATH": "/home/richard/peril" + } + } + } +} \ No newline at end of file diff --git a/test_lslint_mcp.py b/test_lslint_mcp.py new file mode 100755 index 0000000..705aea6 --- /dev/null +++ b/test_lslint_mcp.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +""" +Test script to verify lslint functionality with MCP server +""" + +import json +import subprocess +import sys + +def test_lslint_functionality(): + """Test lslint functionality through MCP server""" + + # Test list_lsl_files first + list_request = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "list_lsl_files", + "arguments": {} + } + } + + try: + # Start the MCP server process + process = subprocess.Popen([ + 'python3', '/home/richard/peril/lsl_mcp_server.py' + ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # Send the request + stdout, stderr = process.communicate(input=json.dumps(list_request) + '\n', timeout=10) + + print("=== LSL MCP Server - LSLint Test ===") + print("\nListing LSL Files:") + + lines = stdout.strip().split('\n') + for line in lines: + if line.strip(): + try: + response = json.loads(line) + if 'content' in response: + print(response['content'][0]['text']) + elif 'tools' in response: + print("Server initialized successfully") + except json.JSONDecodeError: + print(f"Non-JSON output: {line}") + + if stderr: + print(f"\nServer Errors:\n{stderr}") + + print("\n=== Test Complete ===") + + except subprocess.TimeoutExpired: + print("Server test timed out") + process.kill() + except Exception as e: + print(f"Test failed: {e}") + +if __name__ == "__main__": + test_lslint_functionality() \ No newline at end of file diff --git a/test_mcp_server.py b/test_mcp_server.py new file mode 100755 index 0000000..c5b6ebb --- /dev/null +++ b/test_mcp_server.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +""" +Simple test script to verify the LSL MCP server is working +""" + +import json +import subprocess +import sys + +def test_mcp_server(): + """Test basic MCP server functionality""" + + # Test tools/list + test_request = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/list", + "params": {} + } + + try: + # Start the MCP server process + process = subprocess.Popen([ + 'python3', '/home/richard/peril/lsl_mcp_server.py' + ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # Send the request + stdout, stderr = process.communicate(input=json.dumps(test_request) + '\n', timeout=10) + + print("=== MCP Server Test Results ===") + print("\nServer Output:") + for line in stdout.strip().split('\n'): + if line.strip(): + try: + response = json.loads(line) + print(json.dumps(response, indent=2)) + except json.JSONDecodeError: + print(f"Non-JSON output: {line}") + + if stderr: + print(f"\nServer Errors:\n{stderr}") + + print("\n=== Test Complete ===") + + except subprocess.TimeoutExpired: + print("Server test timed out") + process.kill() + except Exception as e: + print(f"Test failed: {e}") + +if __name__ == "__main__": + test_mcp_server() \ No newline at end of file diff --git a/test_validate_lsl.py b/test_validate_lsl.py new file mode 100755 index 0000000..038ed60 --- /dev/null +++ b/test_validate_lsl.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +""" +Test script to validate a specific LSL file using the comprehensive validator +""" + +import json +import subprocess +import sys + +def test_validate_lsl(): + """Test LSL validation through MCP server""" + + # Test validate_lsl_file with Main_Controller_Linkset.lsl + validate_request = { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "validate_lsl_file", + "arguments": { + "file_path": "Main_Controller_Linkset.lsl" + } + } + } + + try: + # Start the MCP server process + process = subprocess.Popen([ + 'python3', '/home/richard/peril/lsl_mcp_server.py' + ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # Send the request + stdout, stderr = process.communicate(input=json.dumps(validate_request) + '\n', timeout=15) + + print("=== LSL Comprehensive Validation Test ===") + print("\nValidating Main_Controller_Linkset.lsl:") + + lines = stdout.strip().split('\n') + for line in lines: + if line.strip(): + try: + response = json.loads(line) + if 'content' in response: + print(response['content'][0]['text']) + elif 'method' in response and response['method'] == 'server/initialized': + print("✅ Server initialized successfully") + except json.JSONDecodeError: + print(f"Non-JSON output: {line}") + + if stderr: + print(f"\nServer Errors:\n{stderr}") + + print("\n=== Test Complete ===") + + except subprocess.TimeoutExpired: + print("Server test timed out") + process.kill() + except Exception as e: + print(f"Test failed: {e}") + +if __name__ == "__main__": + test_validate_lsl() \ No newline at end of file