initial commit
This commit is contained in:
commit
82a11e104c
|
@ -0,0 +1,113 @@
|
|||
# MCP SSH Server
|
||||
|
||||
An Anthropic Model Context Protocol (MCP) server that provides SSH access to remote systems, enabling full access to remote virtual machines in a sandbox environment.
|
||||
|
||||
## Features
|
||||
|
||||
- Uses stdio for MCP communication
|
||||
- Provides SSH connection to remote servers
|
||||
- Enables command execution on remote systems
|
||||
- Secure public key authentication
|
||||
- Configurable via environment variables or MCP config
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The SSH server can be configured using environment variables or the MCP JSON configuration:
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
|----------------------|-------------|---------|
|
||||
| `MCP_SSH_HOSTNAME` | SSH server hostname or IP address | None |
|
||||
| `MCP_SSH_PORT` | SSH server port | 22 |
|
||||
| `MCP_SSH_USERNAME` | SSH username | None |
|
||||
| `MCP_SSH_KEY_FILENAME` | Path to SSH private key file | None |
|
||||
|
||||
### Claude Desktop MCP Configuration
|
||||
|
||||
Add the following to your Claude Desktop MCP configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"name": "mcpssh",
|
||||
"path": "python -m mcpssh",
|
||||
"environment": {
|
||||
"MCP_SSH_HOSTNAME": "example.com",
|
||||
"MCP_SSH_PORT": "22",
|
||||
"MCP_SSH_USERNAME": "user",
|
||||
"MCP_SSH_KEY_FILENAME": "/path/to/private_key"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
This server implements the Anthropic MCP protocol and provides the following tools:
|
||||
|
||||
- `ssh_connect`: Connect to an SSH server using public key authentication (using config or explicit parameters)
|
||||
- `ssh_execute`: Execute a command on the SSH server
|
||||
- `ssh_disconnect`: Disconnect from the SSH server
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcpssh.server import SSHServerMCP
|
||||
|
||||
# Start the server in a subprocess
|
||||
server_params = StdioServerParameters(
|
||||
command="python",
|
||||
args=["-m", "mcpssh"],
|
||||
env={
|
||||
"MCP_SSH_HOSTNAME": "example.com",
|
||||
"MCP_SSH_PORT": "22",
|
||||
"MCP_SSH_USERNAME": "user",
|
||||
"MCP_SSH_KEY_FILENAME": "/path/to/private_key"
|
||||
}
|
||||
)
|
||||
|
||||
# Use with an MCP client
|
||||
with ClientSession(server_params) as client:
|
||||
# Connect to SSH server
|
||||
client.ssh_connect()
|
||||
|
||||
# Execute a command
|
||||
result = client.ssh_execute(command="ls -la")
|
||||
print(result["stdout"])
|
||||
|
||||
# Disconnect
|
||||
client.ssh_disconnect()
|
||||
```
|
||||
|
||||
### Direct Server Usage
|
||||
|
||||
```python
|
||||
from mcpssh.server import SSHServerMCP
|
||||
|
||||
# Initialize and run the server
|
||||
server = SSHServerMCP(
|
||||
hostname="example.com",
|
||||
port=22,
|
||||
username="user",
|
||||
key_filename="/path/to/private_key"
|
||||
)
|
||||
|
||||
# Run the server with stdio transport
|
||||
server.run(transport="stdio")
|
||||
```
|
||||
|
||||
## Security Note
|
||||
|
||||
This tool provides full access to a remote system. It should only be used with virtual machines in sandbox environments where security implications are well understood.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,24 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "mcpssh"
|
||||
version = "0.1.0"
|
||||
description = "Anthropic MCP server that provides SSH access to remote systems"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"paramiko>=3.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
"mcp>=1.6.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"black>=23.0.0",
|
||||
"mypy>=1.0.0",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["mcpssh"]
|
|
@ -0,0 +1,8 @@
|
|||
"""Setup script for mcpssh."""
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="mcpssh",
|
||||
package_dir={"": "src"},
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
Metadata-Version: 2.4
|
||||
Name: mcpssh
|
||||
Version: 0.1.0
|
||||
Summary: Anthropic MCP server that provides SSH access to remote systems
|
||||
Requires-Python: >=3.8
|
||||
Requires-Dist: paramiko>=3.0.0
|
||||
Requires-Dist: pydantic>=2.0.0
|
||||
Requires-Dist: mcp>=1.6.0
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
||||
Requires-Dist: black>=23.0.0; extra == "dev"
|
||||
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
@ -0,0 +1,12 @@
|
|||
README.md
|
||||
pyproject.toml
|
||||
setup.py
|
||||
src/mcpssh/__init__.py
|
||||
src/mcpssh/__main__.py
|
||||
src/mcpssh/server.py
|
||||
src/mcpssh.egg-info/PKG-INFO
|
||||
src/mcpssh.egg-info/SOURCES.txt
|
||||
src/mcpssh.egg-info/dependency_links.txt
|
||||
src/mcpssh.egg-info/requires.txt
|
||||
src/mcpssh.egg-info/top_level.txt
|
||||
tests/test_server.py
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
paramiko>=3.0.0
|
||||
pydantic>=2.0.0
|
||||
mcp>=1.6.0
|
||||
|
||||
[dev]
|
||||
pytest>=7.0.0
|
||||
black>=23.0.0
|
||||
mypy>=1.0.0
|
|
@ -0,0 +1 @@
|
|||
mcpssh
|
|
@ -0,0 +1,3 @@
|
|||
"""MCP SSH Server module."""
|
||||
|
||||
__version__ = "0.1.0"
|
|
@ -0,0 +1,6 @@
|
|||
"""Main entry point for the MCP SSH server."""
|
||||
|
||||
from .server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,222 @@
|
|||
"""MCP SSH Server implementation."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Any, Iterator
|
||||
|
||||
import paramiko
|
||||
from pydantic import BaseModel, Field
|
||||
from mcp import types
|
||||
from mcp.server import FastMCP
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SSHSession:
|
||||
"""SSH Session that connects to a remote server."""
|
||||
|
||||
def __init__(self, hostname: str, port: int, username: str, key_filename: str):
|
||||
"""Initialize SSH session.
|
||||
|
||||
Args:
|
||||
hostname: Remote server hostname
|
||||
port: SSH port
|
||||
username: SSH username
|
||||
key_filename: Path to SSH key file
|
||||
"""
|
||||
self.hostname = hostname
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.key_filename = key_filename
|
||||
self.client = paramiko.SSHClient()
|
||||
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
self.connected = False
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Connect to SSH server.
|
||||
|
||||
Returns:
|
||||
True if connection successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.client.connect(
|
||||
hostname=self.hostname,
|
||||
port=self.port,
|
||||
username=self.username,
|
||||
key_filename=self.key_filename
|
||||
)
|
||||
self.connected = True
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"SSH connection error: {e}")
|
||||
return False
|
||||
|
||||
def execute_command(self, command: str) -> Dict[str, Any]:
|
||||
"""Execute a command on the remote server.
|
||||
|
||||
Args:
|
||||
command: Command to execute
|
||||
|
||||
Returns:
|
||||
Dictionary with stdout, stderr, and exit_code
|
||||
"""
|
||||
if not self.connected:
|
||||
if not self.connect():
|
||||
return {"stdout": "", "stderr": "Not connected to SSH server", "exit_code": -1}
|
||||
|
||||
try:
|
||||
stdin, stdout, stderr = self.client.exec_command(command)
|
||||
exit_code = stdout.channel.recv_exit_status()
|
||||
|
||||
return {
|
||||
"stdout": stdout.read().decode("utf-8"),
|
||||
"stderr": stderr.read().decode("utf-8"),
|
||||
"exit_code": exit_code
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Command execution error: {e}")
|
||||
return {"stdout": "", "stderr": str(e), "exit_code": -1}
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close SSH connection."""
|
||||
if self.connected:
|
||||
self.client.close()
|
||||
self.connected = False
|
||||
|
||||
|
||||
class SSHConnectionParams(BaseModel):
|
||||
"""Parameters for SSH connection."""
|
||||
|
||||
hostname: Optional[str] = Field(None, description="SSH server hostname or IP address")
|
||||
port: Optional[int] = Field(None, description="SSH server port")
|
||||
username: Optional[str] = Field(None, description="SSH username")
|
||||
key_filename: Optional[str] = Field(None, description="Path to SSH private key file")
|
||||
|
||||
|
||||
class CommandParams(BaseModel):
|
||||
"""Parameters for executing a command."""
|
||||
|
||||
command: str = Field(..., description="Command to execute on remote server")
|
||||
|
||||
|
||||
class SSHServerMCP(FastMCP):
|
||||
"""MCP server that provides SSH access to remote systems."""
|
||||
|
||||
def __init__(self, hostname=None, port=None, username=None, key_filename=None, server_name=None, tool_prefix=None):
|
||||
"""Initialize SSH server.
|
||||
|
||||
Args:
|
||||
hostname: SSH server hostname from environment or config
|
||||
port: SSH server port from environment or config
|
||||
username: SSH username from environment or config
|
||||
key_filename: Path to SSH key file from environment or config
|
||||
server_name: Custom name for the server instance (for multiple servers)
|
||||
tool_prefix: Prefix for tool names (e.g., 'server1_' for 'server1_ssh_connect')
|
||||
"""
|
||||
# Get server name from environment variable or parameter
|
||||
server_name = server_name or os.environ.get("MCP_SSH_SERVER_NAME", "SSH Server")
|
||||
super().__init__(name=server_name)
|
||||
self.ssh_session: Optional[SSHSession] = None
|
||||
|
||||
# Get configuration from environment variables or passed parameters
|
||||
self.default_hostname = hostname or os.environ.get("MCP_SSH_HOSTNAME")
|
||||
self.default_port = port or int(os.environ.get("MCP_SSH_PORT", 22))
|
||||
self.default_username = username or os.environ.get("MCP_SSH_USERNAME")
|
||||
self.default_key_filename = key_filename or os.environ.get("MCP_SSH_KEY_FILENAME")
|
||||
|
||||
# Get tool prefix from parameter or environment variable
|
||||
self.tool_prefix = tool_prefix or os.environ.get("MCP_SSH_TOOL_PREFIX", "")
|
||||
|
||||
# Register tools with optional prefix
|
||||
connect_name = f"{self.tool_prefix}ssh_connect"
|
||||
execute_name = f"{self.tool_prefix}ssh_execute"
|
||||
disconnect_name = f"{self.tool_prefix}ssh_disconnect"
|
||||
|
||||
self.add_tool(self.ssh_connect, name=connect_name, description=f"Connect to SSH server: {server_name}")
|
||||
self.add_tool(self.ssh_execute, name=execute_name, description=f"Execute a command on SSH server: {server_name}")
|
||||
self.add_tool(self.ssh_disconnect, name=disconnect_name, description=f"Disconnect from SSH server: {server_name}")
|
||||
|
||||
def ssh_connect(self, params: SSHConnectionParams) -> Dict[str, Any]:
|
||||
"""Connect to an SSH server.
|
||||
|
||||
Args:
|
||||
params: SSH connection parameters (ignored for security-critical fields)
|
||||
|
||||
Returns:
|
||||
Result of connection attempt
|
||||
"""
|
||||
# Always use config/environment variables for security-critical fields
|
||||
hostname = self.default_hostname
|
||||
username = self.default_username
|
||||
key_filename = self.default_key_filename
|
||||
|
||||
# Only allow port to be optionally specified in the request
|
||||
port = self.default_port if self.default_port else params.port
|
||||
|
||||
# Validate that we have all required parameters
|
||||
if not all([hostname, username, key_filename]):
|
||||
missing = []
|
||||
if not hostname: missing.append("hostname")
|
||||
if not username: missing.append("username")
|
||||
if not key_filename: missing.append("key_filename")
|
||||
return {"success": False, "message": f"Missing required parameters: {', '.join(missing)}"}
|
||||
|
||||
self.ssh_session = SSHSession(
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
username=username,
|
||||
key_filename=key_filename
|
||||
)
|
||||
|
||||
if self.ssh_session.connect():
|
||||
return {"success": True, "message": f"Connected to {hostname}"}
|
||||
else:
|
||||
return {"success": False, "message": "Failed to connect to SSH server"}
|
||||
|
||||
def ssh_execute(self, params: CommandParams) -> Dict[str, Any]:
|
||||
"""Execute a command on the SSH server.
|
||||
|
||||
Args:
|
||||
params: Command parameters
|
||||
|
||||
Returns:
|
||||
Command execution result
|
||||
"""
|
||||
if not self.ssh_session or not self.ssh_session.connected:
|
||||
return {"success": False, "message": "Not connected to SSH server"}
|
||||
|
||||
return self.ssh_session.execute_command(params.command)
|
||||
|
||||
def ssh_disconnect(self) -> Dict[str, Any]:
|
||||
"""Disconnect from the SSH server.
|
||||
|
||||
Returns:
|
||||
Disconnect result
|
||||
"""
|
||||
if not self.ssh_session:
|
||||
return {"success": True, "message": "Not connected to SSH server"}
|
||||
|
||||
self.ssh_session.close()
|
||||
return {"success": True, "message": "Disconnected from SSH server"}
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the MCP SSH server."""
|
||||
# Get custom server name and tool prefix from environment if specified
|
||||
server_name = os.environ.get("MCP_SSH_SERVER_NAME")
|
||||
tool_prefix = os.environ.get("MCP_SSH_TOOL_PREFIX")
|
||||
|
||||
# Create server with config from environment variables
|
||||
server = SSHServerMCP(
|
||||
server_name=server_name,
|
||||
tool_prefix=tool_prefix
|
||||
)
|
||||
|
||||
# Run the server with stdio transport
|
||||
server.run(transport="stdio")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
Test script that simulates an MCP client using the SSH server.
|
||||
This helps verify that the server properly responds to MCP requests.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# Set required environment variables for the subprocess
|
||||
env = os.environ.copy()
|
||||
env["MCP_SSH_HOSTNAME"] = "10.0.1.232"
|
||||
env["MCP_SSH_USERNAME"] = "stwhite"
|
||||
env["MCP_SSH_KEY_FILENAME"] = "~/.ssh/id_ed25519"
|
||||
|
||||
# Create temporary files for input/output
|
||||
with tempfile.NamedTemporaryFile('w+') as input_file, tempfile.NamedTemporaryFile('w+') as output_file:
|
||||
# Write an MCP request to connect to SSH server
|
||||
mcp_request = {
|
||||
"type": "request",
|
||||
"id": "1",
|
||||
"method": "ssh_connect",
|
||||
"params": {}
|
||||
}
|
||||
input_file.write(json.dumps(mcp_request) + "\n")
|
||||
input_file.flush()
|
||||
|
||||
# Run the MCP server process with stdin/stdout redirected
|
||||
try:
|
||||
print("Starting MCP SSH server...")
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, "-m", "mcpssh"],
|
||||
env=env,
|
||||
stdin=open(input_file.name, 'r'),
|
||||
stdout=open(output_file.name, 'w'),
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Give it a moment to process
|
||||
stderr_output = process.stderr.read()
|
||||
if stderr_output:
|
||||
print("Error output from server:")
|
||||
print(stderr_output)
|
||||
|
||||
# Kill the process (we're just testing initialization)
|
||||
process.terminate()
|
||||
process.wait()
|
||||
|
||||
# Read server's response
|
||||
output_file.seek(0)
|
||||
response_lines = output_file.readlines()
|
||||
|
||||
if not response_lines:
|
||||
print("No response received from server. This likely indicates an initialization issue.")
|
||||
else:
|
||||
for line in response_lines:
|
||||
try:
|
||||
response = json.loads(line.strip())
|
||||
print("Server response:")
|
||||
print(json.dumps(response, indent=2))
|
||||
except json.JSONDecodeError:
|
||||
print(f"Non-JSON response: {line.strip()}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error running MCP server: {e}")
|
||||
|
||||
print("\nTest complete.")
|
||||
print("For Claude Desktop, use this configuration:")
|
||||
print("""
|
||||
"mcpssh": {
|
||||
"command": "/Volumes/SAM2/CODE/MCP/mcpssh/venv/bin/python",
|
||||
"args": [
|
||||
"-m",
|
||||
"mcpssh"
|
||||
],
|
||||
"env": {
|
||||
"MCP_SSH_HOSTNAME": "10.0.1.232",
|
||||
"MCP_SSH_USERNAME": "stwhite",
|
||||
"MCP_SSH_KEY_FILENAME": "~/.ssh/id_ed25519"
|
||||
}
|
||||
}
|
||||
""")
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
Test script for the MCP SSH server.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from mcpssh.server import SSHServerMCP
|
||||
|
||||
# Set required environment variables for testing
|
||||
os.environ["MCP_SSH_HOSTNAME"] = "10.0.1.232" # Replace with your server
|
||||
os.environ["MCP_SSH_USERNAME"] = "stwhite" # Replace with your username
|
||||
os.environ["MCP_SSH_KEY_FILENAME"] = "~/.ssh/id_ed25519" # Replace with your key path
|
||||
# os.environ["MCP_SSH_PORT"] = "22" # Optional, defaults to 22
|
||||
|
||||
# Create and run the server
|
||||
server = SSHServerMCP()
|
||||
print("Server created with configuration:")
|
||||
print(f"Hostname: {server.default_hostname}")
|
||||
print(f"Port: {server.default_port}")
|
||||
print(f"Username: {server.default_username}")
|
||||
print(f"Key filename: {server.default_key_filename}")
|
||||
print("\nServer ready. In a real scenario, server.run(transport='stdio') would be called.")
|
||||
print("To fully integrate with Claude, add the configuration to Claude Desktop.")
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,198 @@
|
|||
"""Tests for the MCP SSH server."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from mcpssh.server import SSHServerMCP, SSHSession, SSHConnectionParams, CommandParams
|
||||
|
||||
|
||||
class TestSSHSession(unittest.TestCase):
|
||||
"""Test SSH session class."""
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_connect_success(self, mock_ssh_client):
|
||||
"""Test successful SSH connection."""
|
||||
# Setup
|
||||
mock_client = MagicMock()
|
||||
mock_ssh_client.return_value = mock_client
|
||||
|
||||
# Execute
|
||||
session = SSHSession("example.com", 22, "username", "/path/to/key")
|
||||
result = session.connect()
|
||||
|
||||
# Verify
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(session.connected)
|
||||
mock_client.connect.assert_called_once_with(
|
||||
hostname="example.com",
|
||||
port=22,
|
||||
username="username",
|
||||
key_filename="/path/to/key"
|
||||
)
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_connect_failure(self, mock_ssh_client):
|
||||
"""Test failed SSH connection."""
|
||||
# Setup
|
||||
mock_client = MagicMock()
|
||||
mock_client.connect.side_effect = Exception("Connection failed")
|
||||
mock_ssh_client.return_value = mock_client
|
||||
|
||||
# Execute
|
||||
session = SSHSession("example.com", 22, "username", "/path/to/key")
|
||||
result = session.connect()
|
||||
|
||||
# Verify
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(session.connected)
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_execute_command_success(self, mock_ssh_client):
|
||||
"""Test successful command execution."""
|
||||
# Setup
|
||||
mock_client = MagicMock()
|
||||
mock_stdout = MagicMock()
|
||||
mock_stdout.read.return_value = b"command output"
|
||||
mock_stdout.channel.recv_exit_status.return_value = 0
|
||||
mock_stderr = MagicMock()
|
||||
mock_stderr.read.return_value = b""
|
||||
mock_client.exec_command.return_value = (None, mock_stdout, mock_stderr)
|
||||
mock_ssh_client.return_value = mock_client
|
||||
|
||||
# Execute
|
||||
session = SSHSession("example.com", 22, "username", "/path/to/key")
|
||||
session.connected = True # Skip connection
|
||||
session.client = mock_client
|
||||
result = session.execute_command("ls -la")
|
||||
|
||||
# Verify
|
||||
self.assertEqual(result["stdout"], "command output")
|
||||
self.assertEqual(result["stderr"], "")
|
||||
self.assertEqual(result["exit_code"], 0)
|
||||
|
||||
@patch('paramiko.SSHClient')
|
||||
def test_close(self, mock_ssh_client):
|
||||
"""Test closing SSH connection."""
|
||||
# Setup
|
||||
mock_client = MagicMock()
|
||||
mock_ssh_client.return_value = mock_client
|
||||
|
||||
# Execute
|
||||
session = SSHSession("example.com", 22, "username", "/path/to/key")
|
||||
session.connected = True
|
||||
session.client = mock_client
|
||||
session.close()
|
||||
|
||||
# Verify
|
||||
self.assertFalse(session.connected)
|
||||
mock_client.close.assert_called_once()
|
||||
|
||||
|
||||
class TestSSHServerMCP(unittest.TestCase):
|
||||
"""Test SSH server MCP implementation."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.server = SSHServerMCP()
|
||||
|
||||
@patch('mcpssh.server.SSHSession')
|
||||
def test_ssh_connect_success(self, mock_ssh_session):
|
||||
"""Test successful SSH connection."""
|
||||
# Setup
|
||||
mock_session = MagicMock()
|
||||
mock_session.connect.return_value = True
|
||||
mock_ssh_session.return_value = mock_session
|
||||
|
||||
# Execute
|
||||
params = {
|
||||
"hostname": "example.com",
|
||||
"port": 22,
|
||||
"username": "username",
|
||||
"key_filename": "/path/to/key"
|
||||
}
|
||||
result = self.server.ssh_connect(SSHConnectionParams(**params))
|
||||
|
||||
# Verify
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(result["message"], "Connected to example.com")
|
||||
|
||||
@patch('mcpssh.server.SSHSession')
|
||||
def test_ssh_connect_failure(self, mock_ssh_session):
|
||||
"""Test failed SSH connection."""
|
||||
# Setup
|
||||
mock_session = MagicMock()
|
||||
mock_session.connect.return_value = False
|
||||
mock_ssh_session.return_value = mock_session
|
||||
|
||||
# Execute
|
||||
params = {
|
||||
"hostname": "example.com",
|
||||
"port": 22,
|
||||
"username": "username",
|
||||
"key_filename": "/path/to/key"
|
||||
}
|
||||
result = self.server.ssh_connect(SSHConnectionParams(**params))
|
||||
|
||||
# Verify
|
||||
self.assertFalse(result["success"])
|
||||
self.assertEqual(result["message"], "Failed to connect to SSH server")
|
||||
|
||||
def test_ssh_execute_not_connected(self):
|
||||
"""Test command execution when not connected."""
|
||||
# Execute
|
||||
params = {"command": "ls -la"}
|
||||
result = self.server.ssh_execute(CommandParams(**params))
|
||||
|
||||
# Verify
|
||||
self.assertFalse(result["success"])
|
||||
self.assertEqual(result["message"], "Not connected to SSH server")
|
||||
|
||||
@patch('mcpssh.server.SSHSession')
|
||||
def test_ssh_execute_success(self, mock_ssh_session):
|
||||
"""Test successful command execution."""
|
||||
# Setup
|
||||
mock_session = MagicMock()
|
||||
mock_session.connected = True
|
||||
mock_session.execute_command.return_value = {
|
||||
"stdout": "command output",
|
||||
"stderr": "",
|
||||
"exit_code": 0
|
||||
}
|
||||
|
||||
self.server.ssh_session = mock_session
|
||||
|
||||
# Execute
|
||||
params = {"command": "ls -la"}
|
||||
result = self.server.ssh_execute(CommandParams(**params))
|
||||
|
||||
# Verify
|
||||
self.assertEqual(result["stdout"], "command output")
|
||||
self.assertEqual(result["stderr"], "")
|
||||
self.assertEqual(result["exit_code"], 0)
|
||||
|
||||
def test_ssh_disconnect_not_connected(self):
|
||||
"""Test disconnection when not connected."""
|
||||
# Execute
|
||||
result = self.server.ssh_disconnect()
|
||||
|
||||
# Verify
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(result["message"], "Not connected to SSH server")
|
||||
|
||||
def test_ssh_disconnect_success(self):
|
||||
"""Test successful disconnection."""
|
||||
# Setup
|
||||
mock_session = MagicMock()
|
||||
self.server.ssh_session = mock_session
|
||||
|
||||
# Execute
|
||||
result = self.server.ssh_disconnect()
|
||||
|
||||
# Verify
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(result["message"], "Disconnected from SSH server")
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
Reference in New Issue