cnctd-service-ssh 0.1.0

SSH command execution service - library and MCP server
Documentation
//! Tool router for the SSH service.
//!
//! Uses the `rmcp` macros exactly like the template. Tools are thin and
//! delegate to `operations`.

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};

/// Args for registering an SSH target.
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SshRegisterRequest {
    /// Client-defined identifier for this target (unique key).
    pub id: String,
    /// Hostname or IP of the SSH server.
    pub host: String,
    /// Username to authenticate as.
    pub user: String,
    /// SSH port (default 22).
    #[serde(default = "default_port")]
    pub port: u16,
    /// Optional passphrase for the private key.
    pub key_passphrase: Option<String>,
    /// Path to OpenSSH known_hosts file (default "~/.ssh/known_hosts").
    #[serde(default = "default_known_hosts")]
    pub known_hosts_path: String,
}

/// Args for executing a command on a registered target.
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SshExecRequest {
    /// Target id previously registered via `ssh_register_target`.
    pub id: String,
    /// Shell command to execute remotely.
    pub command: String,
    /// Timeout in seconds (default 120).
    #[serde(default = "default_timeout_secs")]
    pub timeout_secs: u64,
}

/// Args for unregistering a target.
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SshUnregisterRequest {
    /// Target id to remove.
    pub id: String,
}

/// SSH service with a ToolRouter, matching the template's structure.
#[derive(Clone)]
pub struct SshToolRouter {
    pub tool_router: ToolRouter<Self>,
}

#[tool_router]
impl SshToolRouter {
    /// Construct the service and initialize the tool router.
    pub fn new() -> Self {
        Self { tool_router: Self::tool_router() }
    }

    /// Register or replace an SSH target configuration.
    #[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)?]))
    }

    /// Execute a command on a registered target; returns stdout, stderr, exit code.
    #[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)?]))
    }

    /// Unregister a target by id.
    #[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()
        }
    }
}

/* ---------- defaults mirror operations.rs for schema consistency ---------- */

fn default_port() -> u16 { 22 }
fn default_known_hosts() -> String { "~/.ssh/known_hosts".into() }
fn default_timeout_secs() -> u64 { 120 }