use rmcp::{
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{CallToolResult, Content, ErrorData as McpError, ServerCapabilities, ServerInfo},
tool, tool_handler, tool_router, ServerHandler, schemars,
};
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,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SshExecRequest {
pub id: String,
pub command: String,
#[serde(default = "default_timeout_secs")]
pub timeout_secs: u64,
}
#[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,
};
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 };
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 }