Files

262 lines
8.0 KiB
Python
Raw Permalink Normal View History

"""SSH MCP Server for remote command execution via SSH.
Provides SSH access to remote hosts for the bot.
"""
import asyncio
from typing import Any, Dict
try:
import paramiko
PARAMIKO_AVAILABLE = True
except ImportError:
PARAMIKO_AVAILABLE = False
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool(
name="ssh_execute",
description="Execute a command on a remote host via SSH. Returns stdout, stderr, and exit code.",
input_schema={
"host": str,
"username": str,
"password": str,
"key_filename": str,
"command": str,
"port": int,
},
)
async def ssh_execute(args: Dict[str, Any]) -> Dict[str, Any]:
"""Execute a command on a remote host via SSH."""
if not PARAMIKO_AVAILABLE:
return {
"content": [{"type": "text", "text": "Error: paramiko not installed. Run: pip install paramiko"}],
"isError": True
}
host = args.get("host")
username = args.get("username")
password = args.get("password")
key_filename = args.get("key_filename")
command = args.get("command")
port = args.get("port", 22)
if not all([host, username, command]):
return {
"content": [{"type": "text", "text": "Error: Missing required parameters: host, username, command"}],
"isError": True
}
if not password and not key_filename:
return {
"content": [{"type": "text", "text": "Error: Must provide either password or key_filename for authentication"}],
"isError": True
}
try:
# Run SSH command in thread pool to avoid blocking
Refactor: Remove zombie code, fix bugs, and clean documentation This comprehensive refactoring removes dead code, fixes bugs, and deletes outdated documentation to make the codebase production-ready. ## Files Deleted (16 files) ### Temporary/zombie files (9 files): - nul (Windows artifact) - quick_start.bat (superseded by run.bat) - scripts/proxmox_ssh.py (hardcoded credentials - security risk) - scripts/proxmox_ssh.sh (hardcoded credentials - security risk) - scripts/collection_output.txt (one-time audit output) - scripts/collect-homelab-config.sh (one-off infrastructure script) - scripts/collect-remote.sh (one-off infrastructure script) - memory_workspace/MEMORY.md.old (backup file) - promtail-config-optimized.yaml (misplaced homelab config) ### Outdated documentation (7 files): - MCP_MIGRATION.md (migration complete - 2026-02-15) - QUICK_REFERENCE_AGENT_SDK.md (orphaned from cleanup) - SETUP.md (duplicate of README.md quick start) - WINDOWS_QUICK_REFERENCE.md (duplicate of docs/WINDOWS_DEPLOYMENT.md) - SUB_AGENTS.md (design doc for unimplemented feature) - JARVIS_VOICE_INTEGRATION_PLAN.md (1300-line spec, code not implemented) - OBSIDIAN_MCP_SETUP_INSTRUCTIONS.md (temporary troubleshooting doc) - LOGGING.md (redundant with well-commented logging_config.py) - docs/SECURITY_AUDIT_SUMMARY.md (completed audit from 2026-02-12) ## Critical Bug Fixes (2 bugs) 1. bot_runner.py line 122: Fixed wrong dict key reference - Changed send_to_platform → send_to - Bug caused scheduled task platform info to never print 2. usage_tracker.py: Added missing pricing for claude-sonnet-4-6 - Model was default but had no pricing entry - Caused cost under-reporting in Direct API mode ## Code Removed (14 files modified, ~1200 lines deleted) ### Dead imports removed (9 imports): - bot_runner.py: sys - agent.py: time - adapters/runtime.py: re - adapters/skill_integration.py: subprocess - tools.py: redundant Path import - mcp_servers/loki/loki_server.py: json - google_tools/oauth_manager.py: Thread, Dict - google_tools/gmail_client.py: os - google_tools/utils.py: email ### Unused functions/methods removed (9 functions): - agent.py: MEMORY_RESPONSE_PREVIEW_LENGTH constant - scheduled_tasks.py: integrate_scheduler_with_runtime() - adapters/runtime.py: command_preprocessor(), markdown_postprocessor() - adapters/skill_integration.py: invoke_skill_via_cli(), __main__ block - tools.py: _extract_mcp_result() - google_tools/oauth_manager.py: needs_refresh_soon(), revoke_authorization() - google_tools/people_client.py: update_contact(), delete_contact() ### Code quality improvements: - memory_system.py: Removed empty else: pass branch - calendar_client.py: Fixed bare except: → except Exception: - mcp_ssh.py: Updated asyncio.get_event_loop() → get_running_loop() - calendar_client.py: Fixed deprecated datetime.utcnow() → now(timezone.utc) ## Impact - ~1200 lines of dead code removed - 16 obsolete files deleted - 2 critical bugs fixed - 3 deprecated APIs updated - Zero functionality broken (all changes verified) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 12:46:56 -07:00
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(
None,
_execute_ssh_sync,
host,
port,
username,
password,
key_filename,
command
)
# Format result as MCP-compliant text content
if result["success"]:
output_parts = [f"SSH command executed on {result['host']} (auth: {result['auth_method']})"]
output_parts.append(f"Exit code: {result['exit_code']}")
if result["stdout"]:
stdout = result["stdout"]
if len(stdout) > 5000:
stdout = stdout[:5000] + "\n... (stdout truncated)"
output_parts.append(f"\nSTDOUT:\n{stdout}")
if result["stderr"]:
stderr = result["stderr"]
if len(stderr) > 5000:
stderr = stderr[:5000] + "\n... (stderr truncated)"
output_parts.append(f"\nSTDERR:\n{stderr}")
if not result["stdout"] and not result["stderr"]:
output_parts.append("\n(no output)")
return {
"content": [{"type": "text", "text": "\n".join(output_parts)}],
"isError": result["exit_code"] != 0
}
else:
return {
"content": [{"type": "text", "text": f"SSH Error: {result['error']}"}],
"isError": True
}
except Exception as e:
return {
"content": [{"type": "text", "text": f"SSH execution failed: {str(e)}"}],
"isError": True
}
def _execute_ssh_sync(host: str, port: int, username: str, password: str, key_filename: str, command: str) -> Dict[str, Any]:
"""Synchronous SSH execution (runs in thread pool)."""
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
# Build connection parameters
connect_kwargs = {
"hostname": host,
"port": port,
"username": username,
"timeout": 30,
}
# Use key-based auth if key_filename provided, otherwise use password
if key_filename:
connect_kwargs["key_filename"] = key_filename
else:
connect_kwargs["password"] = password
client.connect(**connect_kwargs)
stdin, stdout, stderr = client.exec_command(command)
stdout_text = stdout.read().decode('utf-8')
stderr_text = stderr.read().decode('utf-8')
exit_code = stdout.channel.recv_exit_status()
return {
"success": True,
"stdout": stdout_text,
"stderr": stderr_text,
"exit_code": exit_code,
"host": host,
"auth_method": "key" if key_filename else "password",
}
finally:
client.close()
@tool(
name="ssh_file_upload",
description="Upload a file to a remote host via SFTP. Returns success status and file paths.",
input_schema={
"host": str,
"username": str,
"password": str,
"key_filename": str,
"local_path": str,
"remote_path": str,
"port": int,
},
)
async def ssh_file_upload(args: Dict[str, Any]) -> Dict[str, Any]:
"""Upload a file to a remote host via SFTP."""
if not PARAMIKO_AVAILABLE:
return {
"content": [{"type": "text", "text": "Error: paramiko not installed. Run: pip install paramiko"}],
"isError": True
}
host = args.get("host")
username = args.get("username")
password = args.get("password")
key_filename = args.get("key_filename")
local_path = args.get("local_path")
remote_path = args.get("remote_path")
port = args.get("port", 22)
if not all([host, username, local_path, remote_path]):
return {
"content": [{"type": "text", "text": "Error: Missing required parameters: host, username, local_path, remote_path"}],
"isError": True
}
if not password and not key_filename:
return {
"content": [{"type": "text", "text": "Error: Must provide either password or key_filename for authentication"}],
"isError": True
}
try:
Refactor: Remove zombie code, fix bugs, and clean documentation This comprehensive refactoring removes dead code, fixes bugs, and deletes outdated documentation to make the codebase production-ready. ## Files Deleted (16 files) ### Temporary/zombie files (9 files): - nul (Windows artifact) - quick_start.bat (superseded by run.bat) - scripts/proxmox_ssh.py (hardcoded credentials - security risk) - scripts/proxmox_ssh.sh (hardcoded credentials - security risk) - scripts/collection_output.txt (one-time audit output) - scripts/collect-homelab-config.sh (one-off infrastructure script) - scripts/collect-remote.sh (one-off infrastructure script) - memory_workspace/MEMORY.md.old (backup file) - promtail-config-optimized.yaml (misplaced homelab config) ### Outdated documentation (7 files): - MCP_MIGRATION.md (migration complete - 2026-02-15) - QUICK_REFERENCE_AGENT_SDK.md (orphaned from cleanup) - SETUP.md (duplicate of README.md quick start) - WINDOWS_QUICK_REFERENCE.md (duplicate of docs/WINDOWS_DEPLOYMENT.md) - SUB_AGENTS.md (design doc for unimplemented feature) - JARVIS_VOICE_INTEGRATION_PLAN.md (1300-line spec, code not implemented) - OBSIDIAN_MCP_SETUP_INSTRUCTIONS.md (temporary troubleshooting doc) - LOGGING.md (redundant with well-commented logging_config.py) - docs/SECURITY_AUDIT_SUMMARY.md (completed audit from 2026-02-12) ## Critical Bug Fixes (2 bugs) 1. bot_runner.py line 122: Fixed wrong dict key reference - Changed send_to_platform → send_to - Bug caused scheduled task platform info to never print 2. usage_tracker.py: Added missing pricing for claude-sonnet-4-6 - Model was default but had no pricing entry - Caused cost under-reporting in Direct API mode ## Code Removed (14 files modified, ~1200 lines deleted) ### Dead imports removed (9 imports): - bot_runner.py: sys - agent.py: time - adapters/runtime.py: re - adapters/skill_integration.py: subprocess - tools.py: redundant Path import - mcp_servers/loki/loki_server.py: json - google_tools/oauth_manager.py: Thread, Dict - google_tools/gmail_client.py: os - google_tools/utils.py: email ### Unused functions/methods removed (9 functions): - agent.py: MEMORY_RESPONSE_PREVIEW_LENGTH constant - scheduled_tasks.py: integrate_scheduler_with_runtime() - adapters/runtime.py: command_preprocessor(), markdown_postprocessor() - adapters/skill_integration.py: invoke_skill_via_cli(), __main__ block - tools.py: _extract_mcp_result() - google_tools/oauth_manager.py: needs_refresh_soon(), revoke_authorization() - google_tools/people_client.py: update_contact(), delete_contact() ### Code quality improvements: - memory_system.py: Removed empty else: pass branch - calendar_client.py: Fixed bare except: → except Exception: - mcp_ssh.py: Updated asyncio.get_event_loop() → get_running_loop() - calendar_client.py: Fixed deprecated datetime.utcnow() → now(timezone.utc) ## Impact - ~1200 lines of dead code removed - 16 obsolete files deleted - 2 critical bugs fixed - 3 deprecated APIs updated - Zero functionality broken (all changes verified) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 12:46:56 -07:00
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(
None,
_upload_file_sync,
host,
port,
username,
password,
key_filename,
local_path,
remote_path
)
# Format result as MCP-compliant text content
if result["success"]:
text = (
f"File uploaded successfully via SFTP\n"
f"Host: {result['host']}\n"
f"Auth: {result['auth_method']}\n"
f"Local: {result['local_path']}\n"
f"Remote: {result['remote_path']}"
)
return {
"content": [{"type": "text", "text": text}]
}
else:
return {
"content": [{"type": "text", "text": f"SFTP Error: {result['error']}"}],
"isError": True
}
except Exception as e:
return {
"content": [{"type": "text", "text": f"SFTP upload failed: {str(e)}"}],
"isError": True
}
def _upload_file_sync(host: str, port: int, username: str, password: str, key_filename: str, local_path: str, remote_path: str) -> Dict[str, Any]:
"""Synchronous SFTP upload (runs in thread pool)."""
transport = paramiko.Transport((host, port))
try:
# Use key-based auth if key_filename provided, otherwise use password
if key_filename:
private_key = paramiko.RSAKey.from_private_key_file(key_filename)
transport.connect(username=username, pkey=private_key)
else:
transport.connect(username=username, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
sftp.put(local_path, remote_path)
return {
"success": True,
"local_path": local_path,
"remote_path": remote_path,
"host": host,
"auth_method": "key" if key_filename else "password",
}
finally:
sftp.close() if 'sftp' in locals() else None
transport.close()
# Create the MCP server
ssh_mcp_server = create_sdk_mcp_server(
name="ssh",
version="1.0.0",
tools=[ssh_execute, ssh_file_upload],
)