use rmcp::{
ServerHandler,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{CallToolResult, Content, ErrorData as McpError, ServerCapabilities, ServerInfo},
schemars, tool, tool_handler, tool_router,
};
use serde::Deserialize;
use std::collections::HashMap;
use crate::operations::{self, SshExecArgs, SshRegisterArgs};
use crate::sessions::{self, types::*};
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SshRegisterRequest {
pub id: String,
pub host: String,
pub user: String,
#[serde(default = "default_port")]
pub port: u16,
pub key_passphrase: Option<String>,
#[serde(default = "default_known_hosts")]
pub known_hosts_path: String,
#[serde(default)]
pub client_id: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SshExecRequest {
pub id: String,
pub command: String,
#[serde(default = "default_timeout_secs")]
pub timeout_secs: u64,
#[serde(default)]
pub context: Option<String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SshUnregisterRequest {
pub id: String,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ShellSessionCreateRequest {
pub target_id: String,
pub name: Option<String>,
pub shell: Option<String>,
#[serde(default = "default_cols")]
pub cols: u16,
#[serde(default = "default_rows")]
pub rows: u16,
pub client_id: Option<String>,
#[serde(default)]
pub env: HashMap<String, String>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ShellSessionWriteRequest {
pub session_id: String,
pub input: String,
#[serde(default)]
pub newline: bool,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ShellSessionReadRequest {
pub session_id: String,
#[serde(default = "default_format")]
pub format: String,
#[serde(default = "default_true")]
pub consume: bool,
#[serde(default)]
pub wait_ms: u64,
#[serde(default)]
pub min_bytes: usize,
#[serde(default)]
pub wait_for_pattern: Option<String>,
#[serde(default)]
pub wait_for_stable_ms: Option<u64>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ShellSessionListRequest {
pub target_id: Option<String>,
pub client_id: Option<String>,
#[serde(default = "default_true")]
pub include_disconnected: bool,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ShellSessionReconnectRequest {
pub session_id: String,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ShellSessionResizeRequest {
pub session_id: String,
pub cols: u16,
pub rows: u16,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct ShellSessionCloseRequest {
pub session_id: String,
#[serde(default)]
pub force: bool,
}
#[derive(Clone)]
pub struct SshToolRouter {
pub tool_router: ToolRouter<Self>,
}
#[tool_router]
impl SshToolRouter {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
#[tool(description = "Register or replace an SSH target configuration")]
async fn ssh_register_target(
&self,
Parameters(req): Parameters<SshRegisterRequest>,
) -> Result<CallToolResult, McpError> {
let args = SshRegisterArgs {
id: req.id,
host: req.host,
user: req.user,
port: req.port,
key_passphrase: req.key_passphrase,
known_hosts_path: req.known_hosts_path,
client_id: req.client_id,
};
let v = operations::ssh_register(args).await?;
Ok(CallToolResult::success(vec![Content::json(v)?]))
}
#[tool(description = "Execute a command on a registered target")]
async fn ssh_exec(
&self,
Parameters(req): Parameters<SshExecRequest>,
) -> Result<CallToolResult, McpError> {
let args = SshExecArgs {
id: req.id,
command: req.command,
timeout_secs: req.timeout_secs,
context: req.context,
};
let v = operations::ssh_exec(args).await?;
Ok(CallToolResult::success(vec![Content::json(v)?]))
}
#[tool(description = "Unregister a previously registered target")]
async fn ssh_unregister_target(
&self,
Parameters(req): Parameters<SshUnregisterRequest>,
) -> Result<CallToolResult, McpError> {
let v = operations::ssh_unregister(req.id).await?;
Ok(CallToolResult::success(vec![Content::json(v)?]))
}
#[tool(description = "Create a new interactive shell session on a registered SSH target. Sessions are persistent and survive disconnections.")]
async fn shell_session_create(
&self,
Parameters(req): Parameters<ShellSessionCreateRequest>,
) -> Result<CallToolResult, McpError> {
let args = ShellSessionCreateArgs {
target_id: req.target_id,
name: req.name,
shell: req.shell,
cols: req.cols,
rows: req.rows,
client_id: req.client_id,
env: req.env,
};
let result = sessions::shell_session_create(args).await?;
Ok(CallToolResult::success(vec![Content::json(serde_json::to_value(result).map_err(|e| {
McpError::internal_error(format!("JSON error: {}", e), None)
})?)?]))
}
#[tool(description = "Send input to an interactive shell session. Use for commands, keystrokes (\\x03 for Ctrl+C), or any terminal input.")]
async fn shell_session_write(
&self,
Parameters(req): Parameters<ShellSessionWriteRequest>,
) -> Result<CallToolResult, McpError> {
let args = ShellSessionWriteArgs {
session_id: req.session_id,
input: req.input,
newline: req.newline,
};
let result = sessions::shell_session_write(args).await?;
Ok(CallToolResult::success(vec![Content::json(serde_json::to_value(result).map_err(|e| {
McpError::internal_error(format!("JSON error: {}", e), None)
})?)?]))
}
#[tool(description = "Read output from an interactive shell session. Supports raw output, screen state (for TUI apps), stripped (no ANSI codes), or both. Use wait_for_pattern to wait for a prompt, or wait_for_stable_ms to wait for output to stop changing.")]
async fn shell_session_read(
&self,
Parameters(req): Parameters<ShellSessionReadRequest>,
) -> Result<CallToolResult, McpError> {
let format = match req.format.to_lowercase().as_str() {
"raw" => OutputFormat::Raw,
"screen" => OutputFormat::Screen,
"both" => OutputFormat::Both,
"stripped" => OutputFormat::Stripped,
_ => OutputFormat::Raw, };
let args = ShellSessionReadArgs {
session_id: req.session_id,
format,
consume: req.consume,
wait_ms: req.wait_ms,
min_bytes: req.min_bytes,
wait_for_pattern: req.wait_for_pattern,
wait_for_stable_ms: req.wait_for_stable_ms,
};
let result = sessions::shell_session_read(args).await?;
Ok(CallToolResult::success(vec![Content::json(serde_json::to_value(result).map_err(|e| {
McpError::internal_error(format!("JSON error: {}", e), None)
})?)?]))
}
#[tool(description = "List interactive shell sessions. Can filter by target or client ID.")]
async fn shell_session_list(
&self,
Parameters(req): Parameters<ShellSessionListRequest>,
) -> Result<CallToolResult, McpError> {
let args = ShellSessionListArgs {
target_id: req.target_id,
client_id: req.client_id,
include_disconnected: req.include_disconnected,
};
let result = sessions::shell_session_list(args).await?;
Ok(CallToolResult::success(vec![Content::json(serde_json::to_value(result).map_err(|e| {
McpError::internal_error(format!("JSON error: {}", e), None)
})?)?]))
}
#[tool(description = "Reconnect to a disconnected shell session. The session must still exist on the remote server.")]
async fn shell_session_reconnect(
&self,
Parameters(req): Parameters<ShellSessionReconnectRequest>,
) -> Result<CallToolResult, McpError> {
let args = ShellSessionReconnectArgs {
session_id: req.session_id,
};
let result = sessions::shell_session_reconnect(args).await?;
Ok(CallToolResult::success(vec![Content::json(serde_json::to_value(result).map_err(|e| {
McpError::internal_error(format!("JSON error: {}", e), None)
})?)?]))
}
#[tool(description = "Resize a shell session's terminal dimensions. Important for TUI applications.")]
async fn shell_session_resize(
&self,
Parameters(req): Parameters<ShellSessionResizeRequest>,
) -> Result<CallToolResult, McpError> {
let args = ShellSessionResizeArgs {
session_id: req.session_id,
cols: req.cols,
rows: req.rows,
};
let result = sessions::shell_session_resize(args).await?;
Ok(CallToolResult::success(vec![Content::json(serde_json::to_value(result).map_err(|e| {
McpError::internal_error(format!("JSON error: {}", e), None)
})?)?]))
}
#[tool(description = "Close an interactive shell session. Terminates the remote shell.")]
async fn shell_session_close(
&self,
Parameters(req): Parameters<ShellSessionCloseRequest>,
) -> Result<CallToolResult, McpError> {
let args = ShellSessionCloseArgs {
session_id: req.session_id,
force: req.force,
};
let result = sessions::shell_session_close(args).await?;
Ok(CallToolResult::success(vec![Content::json(serde_json::to_value(result).map_err(|e| {
McpError::internal_error(format!("JSON error: {}", e), None)
})?)?]))
}
}
impl Default for SshToolRouter {
fn default() -> Self {
Self::new()
}
}
#[tool_handler]
impl ServerHandler for SshToolRouter {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some("cnctd-service-ssh: secure SSH command execution with enforced host key verification and whitelisted keys".into()),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
fn default_port() -> u16 {
22
}
fn default_known_hosts() -> String {
"~/.ssh/known_hosts".into()
}
fn default_timeout_secs() -> u64 {
120
}
fn default_cols() -> u16 {
120
}
fn default_rows() -> u16 {
40
}
fn default_true() -> bool {
true
}
fn default_format() -> String {
"raw".to_string()
}