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:
@@ -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)}"}
|
||||
|
||||
Reference in New Issue
Block a user