pty-mcp 0.3.0

An MCP server for PTY management with SSH connections, remote sessions, file access, and mounts
Documentation
use std::sync::Arc;

use rmcp::{
    ErrorData as McpError, ServerHandler,
    handler::server::router::tool::ToolRouter,
    model::{
        ListResourceTemplatesResult, ListResourcesResult, PaginatedRequestParams,
        ReadResourceRequestParams, ReadResourceResult, ServerCapabilities, ServerInfo,
        TasksCapability, Tool,
    },
    service::{RequestContext, RoleServer},
    task_handler, tool_handler,
};

use crate::{
    AppState,
    mcp::{resources, tasks},
};

#[derive(Clone)]
pub struct PtyMcpServer {
    app: Arc<AppState>,
    tool_router: ToolRouter<Self>,
    processor: tasks::TaskProcessor,
}

impl PtyMcpServer {
    pub fn new(app: Arc<AppState>) -> Self {
        let mut tool_router = Self::tool_router();
        if !app.ssh_mount_feature_available() {
            tool_router.remove_route("ssh_mount");
            tool_router.remove_route("ssh_unmount");
        }

        Self {
            app,
            tool_router,
            processor: tasks::new_task_processor(),
        }
    }

    pub fn app(&self) -> &Arc<AppState> {
        &self.app
    }

    pub fn tool_definitions(&self) -> Vec<Tool> {
        self.tool_router.list_all().to_vec()
    }
}

#[tool_handler(router = self.tool_router)]
#[task_handler]
impl ServerHandler for PtyMcpServer {
    fn get_info(&self) -> ServerInfo {
        let mut capabilities = ServerCapabilities::builder()
            .enable_tools()
            .enable_resources()
            .build();
        capabilities.tasks = Some(TasksCapability::server_default());

        let ssh_tools = if self.app().ssh_mount_feature_available() {
            "ssh_connect, ssh_session_spawn, ssh_exec, ssh_run, ssh_read_file, \
             ssh_write_file, ssh_list_dir, ssh_mkdir, ssh_tunnel_open, \
             ssh_tunnel_close, ssh_mount, ssh_unmount, ssh_list, and ssh_disconnect"
        } else {
            "ssh_connect, ssh_session_spawn, ssh_exec, ssh_run, ssh_read_file, \
             ssh_write_file, ssh_list_dir, ssh_mkdir, ssh_tunnel_open, \
             ssh_tunnel_close, ssh_list, and ssh_disconnect"
        };
        let ssh_resources = if self.app().ssh_mount_feature_available() {
            "ssh://connections, ssh://tunnels, and ssh://mounts"
        } else {
            "ssh://connections and ssh://tunnels"
        };

        ServerInfo::new(capabilities).with_instructions(
            format!(
                "Manage PTY sessions through tools. Use pty_spawn, pty_write, pty_read, \
                 pty_list, pty_kill, and pty_wait for the main PTY workflow. PTY reads are \
                 agent-first: default to compact page.text, request line_number_mode=embedded \
                 only when precise line references matter, and set capture_limit only when you \
                 need initial_output. output_view=raw cannot be combined with \
                 line_number_mode=embedded. Use {ssh_tools} to manage SSH connections, remote \
                 sessions, remote files, tunnel summaries, and mount summaries. ssh_connect \
                 requires explicit auth_kind. SSH tunnels default to \
                 bind_host=127.0.0.1 and remote_host=127.0.0.1; non-loopback bind_host values must \
                 be explicitly allowed by policy. Resources expose read-only snapshots, including \
                 pty://sessions plus {ssh_resources}. \
                 When SSH mount support is unavailable or a mount fails because local prerequisites \
                 are missing, read ssh://docs/mount-setup and the matching \
                 ssh://docs/mount-setup/{{platform}} guide before suggesting installation steps. \
                 Tasks are available as an optional enhancement."
            ),
        )
    }

    fn list_resources(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<ListResourcesResult, McpError>> + Send + '_ {
        std::future::ready(Ok(resources::list_resources(self.app())))
    }

    fn list_resource_templates(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<ListResourceTemplatesResult, McpError>> + Send + '_ {
        std::future::ready(Ok(resources::list_resource_templates(self.app())))
    }

    fn read_resource(
        &self,
        request: ReadResourceRequestParams,
        _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<ReadResourceResult, McpError>> + Send + '_ {
        std::future::ready(resources::read_resource(self.app(), &request.uri))
    }
}