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 crate::operations::{self, SshExecArgs, SshRegisterArgs};
#[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(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)?]))
}
}
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
}