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:
@@ -41,6 +41,7 @@ if not logger.handlers:
|
||||
# Try to import Agent SDK (optional dependency)
|
||||
try:
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ResultMessage,
|
||||
)
|
||||
@@ -345,8 +346,12 @@ class LLMInterface:
|
||||
"""
|
||||
try:
|
||||
from mcp_tools import file_system_server
|
||||
from mcp_servers.mcp_ssh import ssh_mcp_server
|
||||
|
||||
mcp_servers = {"file_system": file_system_server}
|
||||
mcp_servers = {
|
||||
"file_system": file_system_server,
|
||||
"ssh": ssh_mcp_server,
|
||||
}
|
||||
|
||||
# All tools registered in the MCP server
|
||||
allowed_tools = [
|
||||
@@ -356,6 +361,9 @@ class LLMInterface:
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"run_command",
|
||||
# SSH tools
|
||||
"ssh_execute",
|
||||
"ssh_file_upload",
|
||||
# Web tool
|
||||
"web_fetch",
|
||||
# Zettelkasten tools
|
||||
@@ -404,6 +412,46 @@ class LLMInterface:
|
||||
except Exception as e:
|
||||
print(f"[LLM] Obsidian MCP unavailable: {e}")
|
||||
|
||||
# Conditionally add Cloudflare Code Mode MCP server
|
||||
try:
|
||||
from mcp_servers.cloudflare.cloudflare_mcp import (
|
||||
is_cloudflare_enabled,
|
||||
get_cloudflare_server_config,
|
||||
CLOUDFLARE_TOOLS,
|
||||
)
|
||||
|
||||
if is_cloudflare_enabled():
|
||||
cloudflare_config = get_cloudflare_server_config()
|
||||
mcp_servers["cloudflare"] = cloudflare_config
|
||||
allowed_tools.extend(CLOUDFLARE_TOOLS)
|
||||
print("[LLM] Cloudflare MCP server registered (2 tools: search, execute)")
|
||||
else:
|
||||
print("[LLM] Cloudflare MCP disabled or no API token set")
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[LLM] Cloudflare MCP unavailable: {e}")
|
||||
|
||||
# Conditionally add Loki MCP server (homelab log querying)
|
||||
try:
|
||||
from mcp_servers.loki.loki_mcp import (
|
||||
is_loki_enabled,
|
||||
get_loki_server_config,
|
||||
LOKI_TOOLS,
|
||||
)
|
||||
|
||||
if is_loki_enabled():
|
||||
loki_config = get_loki_server_config()
|
||||
mcp_servers["loki"] = loki_config
|
||||
allowed_tools.extend(LOKI_TOOLS)
|
||||
print(f"[LLM] Loki MCP server registered ({len(LOKI_TOOLS)} tools: {', '.join(LOKI_TOOLS)})")
|
||||
else:
|
||||
print("[LLM] Loki MCP disabled")
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[LLM] Loki MCP unavailable: {e}")
|
||||
|
||||
def _stderr_callback(line: str) -> None:
|
||||
"""Log Claude CLI stderr for debugging transport failures."""
|
||||
logger.debug("[CLI stderr] %s", line)
|
||||
@@ -517,15 +565,20 @@ class LLMInterface:
|
||||
|
||||
# Collect text from AssistantMessage objects
|
||||
if isinstance(message, AssistantMessage):
|
||||
logger.debug(f"[LLM] AssistantMessage: has_content={hasattr(message, 'content')}")
|
||||
if hasattr(message, 'content') and message.content:
|
||||
# Extract text from content blocks
|
||||
if isinstance(message.content, str):
|
||||
assistant_messages.append(message.content)
|
||||
logger.debug(f"[LLM] → Collected string: {len(message.content)} chars")
|
||||
elif isinstance(message.content, list):
|
||||
for block in message.content:
|
||||
if hasattr(block, 'type') and block.type == 'text':
|
||||
if hasattr(block, 'text'):
|
||||
assistant_messages.append(block.text)
|
||||
logger.debug(f"[LLM] → Collected text block: {len(block.text)} chars")
|
||||
else:
|
||||
logger.debug(f"[LLM] → AssistantMessage has no content or empty")
|
||||
|
||||
if isinstance(message, ResultMessage):
|
||||
# Use ResultMessage.result if available, otherwise use collected assistant messages
|
||||
@@ -537,7 +590,9 @@ class LLMInterface:
|
||||
getattr(message, "num_turns", "?"),
|
||||
)
|
||||
if not message.result and assistant_messages:
|
||||
logger.debug(f"[LLM] ResultMessage.result was empty, using {len(assistant_messages)} collected assistant messages")
|
||||
logger.info(f"[LLM] ResultMessage.result was empty, using {len(assistant_messages)} collected assistant messages")
|
||||
elif not message.result and not assistant_messages:
|
||||
logger.warning(f"[LLM] PROBLEM: Both ResultMessage.result and assistant_messages are empty!")
|
||||
break
|
||||
|
||||
# Log non-result messages to detect loops
|
||||
|
||||
Reference in New Issue
Block a user