Add SSH MCP server and Gmail attachment download

Features:
- SSH MCP server with two tools:
  * ssh_execute: Run commands on remote hosts via SSH
  * ssh_file_upload: Upload files via SFTP
- Support for both password and SSH key authentication
- Auto-accept SSH host keys (AutoAddPolicy) for homelab use
- Gmail attachment download functionality
- Added download_attachment tool for Gmail API

Technical changes:
- Created mcp_servers/mcp_ssh.py with MCP-compliant text output
- Updated llm_interface.py to load SSH MCP server
- Added paramiko>=3.4.0 to requirements.txt
- Updated .env.example with SSH configuration template
- Enhanced gmail_client.py with download_attachment() method
- Added download_attachment tool handler in tools.py

SSH credentials configured via environment variables:
- PROXMOX_SSH_HOST, PROXMOX_SSH_USER, PROXMOX_SSH_PORT
- PROXMOX_SSH_PASSWORD (or) PROXMOX_SSH_KEY_FILE

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 12:32:05 -07:00
parent a9efdc0a01
commit 58de3e55dc
6 changed files with 489 additions and 10 deletions

View File

@@ -1,5 +1,8 @@
"""Gmail API client for sending and reading emails."""
import base64
import os
from pathlib import Path
from typing import Dict, List, Optional
from googleapiclient.discovery import build
@@ -201,13 +204,27 @@ class GmailClient:
# Get attachment info if any
payload = message.get("payload", {})
attachments = []
for part in payload.get("parts", []):
if part.get("filename"):
attachments.append({
"filename": part["filename"],
"mime_type": part.get("mimeType"),
"size": part.get("body", {}).get("size", 0),
})
def extract_attachments(parts):
"""Recursively extract attachments from message parts."""
for part in parts:
filename = part.get("filename")
if filename:
body = part.get("body", {})
attachment_id = body.get("attachmentId")
if attachment_id:
attachments.append({
"filename": filename,
"attachment_id": attachment_id,
"mime_type": part.get("mimeType"),
"size": body.get("size", 0),
})
# Recursively check nested parts
if "parts" in part:
extract_attachments(part["parts"])
if "parts" in payload:
extract_attachments(payload["parts"])
email_data["attachments"] = attachments
@@ -218,3 +235,62 @@ class GmailClient:
except HttpError as e:
return {"success": False, "error": str(e)}
def download_attachment(
self,
message_id: str,
attachment_id: str,
filename: str,
output_dir: str = "downloads",
) -> Dict:
"""Download an email attachment.
Args:
message_id: Gmail message ID
attachment_id: Attachment ID from the message
filename: Original filename of the attachment
output_dir: Directory to save the attachment (default: "downloads")
Returns:
Dict with success status and file path or error
"""
if not self.service:
if not self._initialize_service():
return {
"success": False,
"error": "Not authorized. Run: python bot_runner.py --setup-google",
}
try:
# Get the attachment data
attachment = (
self.service.users()
.messages()
.attachments()
.get(userId="me", messageId=message_id, id=attachment_id)
.execute()
)
# Decode the attachment data
file_data = base64.urlsafe_b64decode(attachment["data"])
# Create output directory if it doesn't exist
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# Save the file
file_path = output_path / filename
with open(file_path, "wb") as f:
f.write(file_data)
return {
"success": True,
"file_path": str(file_path),
"filename": filename,
"size": len(file_data),
}
except HttpError as e:
return {"success": False, "error": str(e)}
except Exception as e:
return {"success": False, "error": f"Failed to save attachment: {str(e)}"}