Skip to main content
Glama
bvisible

MCP SSH Manager

ssh_tunnel_create

Create SSH tunnels for secure port forwarding or SOCKS proxy connections between local and remote systems.

Instructions

Create SSH tunnel (port forwarding or SOCKS proxy)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
serverYesServer name or alias
typeYesTunnel type
localHostNoLocal host (default: 127.0.0.1)
localPortYesLocal port
remoteHostNoRemote host (not needed for dynamic)
remotePortNoRemote port (not needed for dynamic)

Implementation Reference

  • Registration of 'ssh_tunnel_create' tool in the 'advanced' group of the tool registry, used for conditional tool registration based on user configuration.
    advanced: [
      'ssh_deploy',
      'ssh_execute_sudo',
      'ssh_alias',
      'ssh_command_alias',
      'ssh_hooks',
      'ssh_profile',
      'ssh_connection_status',
      'ssh_tunnel_create',
      'ssh_tunnel_list',
      'ssh_tunnel_close',
      'ssh_key_manage',
      'ssh_execute_group',
      'ssh_group_manage',
      'ssh_history'
    ]
  • Core handler function that creates SSH tunnels of type local, remote, or dynamic (SOCKS). Validates input config, creates SSHTunnel instance, starts forwarding, and returns the tunnel object. This implements the logic for the 'ssh_tunnel_create' tool.
    export async function createTunnel(serverName, ssh, config) {
      const tunnelId = `tunnel_${Date.now()}_${uuidv4().substring(0, 8)}`;
    
      // Validate config
      if (!config.type || !Object.values(TUNNEL_TYPES).includes(config.type)) {
        throw new Error(`Invalid tunnel type: ${config.type}`);
      }
    
      // Set defaults
      config.localHost = config.localHost || '127.0.0.1';
    
      if (config.type !== TUNNEL_TYPES.DYNAMIC) {
        if (!config.remoteHost || !config.remotePort) {
          throw new Error('Remote host and port required for port forwarding');
        }
      }
    
      if (!config.localPort) {
        throw new Error('Local port required');
      }
    
      const tunnel = new SSHTunnel(tunnelId, serverName, ssh, config);
      tunnels.set(tunnelId, tunnel);
    
      try {
        await tunnel.start();
    
        logger.info('SSH tunnel created', {
          id: tunnelId,
          type: config.type,
          server: serverName
        });
    
        return tunnel;
      } catch (error) {
        tunnels.delete(tunnelId);
        throw error;
      }
    }
  • Schema defining supported tunnel types (local, remote, dynamic) used for input validation in tunnel creation.
    export const TUNNEL_TYPES = {
      LOCAL: 'local',        // Local port forwarding (access remote service locally)
      REMOTE: 'remote',      // Remote port forwarding (expose local service remotely)
      DYNAMIC: 'dynamic'     // SOCKS proxy
    };
  • SSHTunnel class providing full tunnel lifecycle management: starting forwarding servers, handling connections, data piping, stats tracking, reconnection, and cleanup. Core supporting utility for tunnel operations.
    class SSHTunnel {
      constructor(id, serverName, ssh, config) {
        this.id = id;
        this.serverName = serverName;
        this.ssh = ssh;
        this.type = config.type;
        this.config = config;
        this.state = TUNNEL_STATES.CONNECTING;
        this.createdAt = new Date();
        this.lastActivity = new Date();
        this.connections = new Set();
        this.server = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.stats = {
          bytesTransferred: 0,
          connectionsTotal: 0,
          connectionsActive: 0,
          errors: 0
        };
      }
    
      /**
       * Start the tunnel
       */
      async start() {
        try {
          switch (this.type) {
          case TUNNEL_TYPES.LOCAL:
            await this.startLocalForwarding();
            break;
    
          case TUNNEL_TYPES.REMOTE:
            await this.startRemoteForwarding();
            break;
    
          case TUNNEL_TYPES.DYNAMIC:
            await this.startDynamicForwarding();
            break;
    
          default:
            throw new Error(`Unknown tunnel type: ${this.type}`);
          }
    
          this.state = TUNNEL_STATES.ACTIVE;
          this.lastActivity = new Date();
    
          logger.info(`SSH tunnel ${this.id} started`, {
            type: this.type,
            server: this.serverName,
            local: `${this.config.localHost}:${this.config.localPort}`,
            remote: this.type !== TUNNEL_TYPES.DYNAMIC ?
              `${this.config.remoteHost}:${this.config.remotePort}` : 'SOCKS'
          });
    
        } catch (error) {
          this.state = TUNNEL_STATES.FAILED;
          logger.error(`Failed to start tunnel ${this.id}`, {
            error: error.message
          });
          throw error;
        }
      }
    
      /**
       * Start local port forwarding
       */
      async startLocalForwarding() {
        const { localHost, localPort, remoteHost, remotePort } = this.config;
    
        // Create local server
        this.server = net.createServer(async (localSocket) => {
          this.stats.connectionsTotal++;
          this.stats.connectionsActive++;
          this.connections.add(localSocket);
          this.lastActivity = new Date();
    
          logger.debug(`New connection to tunnel ${this.id}`, {
            from: localSocket.remoteAddress
          });
    
          try {
            // Forward to remote via SSH
            const stream = await this.ssh.forwardOut(
              localSocket.remoteAddress || '127.0.0.1',
              localSocket.remotePort || 0,
              remoteHost,
              remotePort
            );
    
            // Pipe data between local and remote
            localSocket.pipe(stream).pipe(localSocket);
    
            // Track data transfer
            localSocket.on('data', (chunk) => {
              this.stats.bytesTransferred += chunk.length;
              this.lastActivity = new Date();
            });
    
            stream.on('data', (chunk) => {
              this.stats.bytesTransferred += chunk.length;
              this.lastActivity = new Date();
            });
    
            // Handle disconnection
            const cleanup = () => {
              this.stats.connectionsActive--;
              this.connections.delete(localSocket);
              localSocket.destroy();
              stream.destroy();
            };
    
            localSocket.on('close', cleanup);
            localSocket.on('error', cleanup);
            stream.on('close', cleanup);
            stream.on('error', cleanup);
    
          } catch (error) {
            this.stats.errors++;
            logger.error('Tunnel forwarding error', {
              tunnel: this.id,
              error: error.message
            });
            localSocket.destroy();
          }
        });
    
        // Start listening
        await new Promise((resolve, reject) => {
          this.server.listen(localPort, localHost, (err) => {
            if (err) reject(err);
            else resolve();
          });
        });
    
        logger.info('Local forwarding established', {
          local: `${localHost}:${localPort}`,
          remote: `${remoteHost}:${remotePort}`
        });
      }
    
      /**
       * Start remote port forwarding
       */
      async startRemoteForwarding() {
        const { localHost, localPort, remoteHost, remotePort } = this.config;
    
        // Request remote forwarding from SSH server
        await new Promise((resolve, reject) => {
          this.ssh.forwardIn(remoteHost, remotePort, (err) => {
            if (err) reject(err);
            else resolve();
          });
        });
    
        // Handle incoming connections from remote
        this.ssh.on('tcp connection', (info, accept, reject) => {
          if (info.destPort !== remotePort) return;
    
          this.stats.connectionsTotal++;
          this.stats.connectionsActive++;
          this.lastActivity = new Date();
    
          const remoteSocket = accept();
    
          // Connect to local service
          const localSocket = net.connect(localPort, localHost, () => {
            // Pipe data between remote and local
            remoteSocket.pipe(localSocket).pipe(remoteSocket);
    
            // Track data transfer
            remoteSocket.on('data', (chunk) => {
              this.stats.bytesTransferred += chunk.length;
              this.lastActivity = new Date();
            });
    
            localSocket.on('data', (chunk) => {
              this.stats.bytesTransferred += chunk.length;
              this.lastActivity = new Date();
            });
          });
    
          // Handle errors and cleanup
          const cleanup = () => {
            this.stats.connectionsActive--;
            remoteSocket.destroy();
            localSocket.destroy();
          };
    
          localSocket.on('error', (err) => {
            this.stats.errors++;
            logger.error('Remote forwarding error', {
              tunnel: this.id,
              error: err.message
            });
            cleanup();
          });
    
          remoteSocket.on('close', cleanup);
          localSocket.on('close', cleanup);
        });
    
        logger.info('Remote forwarding established', {
          local: `${localHost}:${localPort}`,
          remote: `${remoteHost}:${remotePort}`
        });
      }
    
      /**
       * Start dynamic port forwarding (SOCKS proxy)
       */
      async startDynamicForwarding() {
        const { localHost, localPort } = this.config;
    
        // Create SOCKS server
        this.server = net.createServer(async (localSocket) => {
          this.stats.connectionsTotal++;
          this.stats.connectionsActive++;
          this.connections.add(localSocket);
          this.lastActivity = new Date();
    
          let targetHost = null;
          let targetPort = null;
          let stream = null;
    
          // Simple SOCKS5 implementation (basic)
          localSocket.once('data', async (chunk) => {
            // Parse SOCKS request (simplified)
            if (chunk[0] === 0x05) { // SOCKS5
              // Send auth method response
              localSocket.write(Buffer.from([0x05, 0x00]));
    
              localSocket.once('data', async (chunk2) => {
                // Parse connection request
                if (chunk2[0] === 0x05 && chunk2[1] === 0x01) { // CONNECT
                  const addrType = chunk2[3];
                  let offset = 4;
    
                  if (addrType === 0x01) { // IPv4
                    targetHost = `${chunk2[4]}.${chunk2[5]}.${chunk2[6]}.${chunk2[7]}`;
                    offset = 8;
                  } else if (addrType === 0x03) { // Domain
                    const domainLen = chunk2[4];
                    targetHost = chunk2.slice(5, 5 + domainLen).toString();
                    offset = 5 + domainLen;
                  }
    
                  targetPort = (chunk2[offset] << 8) | chunk2[offset + 1];
    
                  try {
                    // Create SSH forwarding stream
                    stream = await this.ssh.forwardOut(
                      '127.0.0.1', 0,
                      targetHost, targetPort
                    );
    
                    // Send success response
                    const response = Buffer.from([
                      0x05, 0x00, 0x00, 0x01,
                      0, 0, 0, 0,  // Bind address (0.0.0.0)
                      0, 0         // Bind port
                    ]);
                    localSocket.write(response);
    
                    // Pipe data
                    localSocket.pipe(stream).pipe(localSocket);
    
                    // Track data
                    localSocket.on('data', (chunk) => {
                      this.stats.bytesTransferred += chunk.length;
                      this.lastActivity = new Date();
                    });
    
                    stream.on('data', (chunk) => {
                      this.stats.bytesTransferred += chunk.length;
                      this.lastActivity = new Date();
                    });
    
                  } catch (error) {
                    // Send error response
                    const response = Buffer.from([
                      0x05, 0x01, 0x00, 0x01,
                      0, 0, 0, 0, 0, 0
                    ]);
                    localSocket.write(response);
                    localSocket.destroy();
                    this.stats.errors++;
                  }
                }
              });
            } else {
              // Not SOCKS5, close connection
              localSocket.destroy();
            }
          });
    
          // Cleanup on disconnect
          localSocket.on('close', () => {
            this.stats.connectionsActive--;
            this.connections.delete(localSocket);
            if (stream) stream.destroy();
          });
    
          localSocket.on('error', () => {
            this.stats.errors++;
            this.stats.connectionsActive--;
            this.connections.delete(localSocket);
            if (stream) stream.destroy();
          });
        });
    
        // Start listening
        await new Promise((resolve, reject) => {
          this.server.listen(localPort, localHost, (err) => {
            if (err) reject(err);
            else resolve();
          });
        });
    
        logger.info('SOCKS proxy established', {
          local: `${localHost}:${localPort}`
        });
      }
    
      /**
       * Get tunnel information
       */
      getInfo() {
        return {
          id: this.id,
          server: this.serverName,
          type: this.type,
          state: this.state,
          config: {
            localHost: this.config.localHost,
            localPort: this.config.localPort,
            remoteHost: this.config.remoteHost,
            remotePort: this.config.remotePort
          },
          stats: this.stats,
          created: this.createdAt,
          lastActivity: this.lastActivity,
          activeConnections: this.connections.size
        };
      }
    
      /**
       * Close the tunnel
       */
      close() {
        logger.info(`Closing tunnel ${this.id}`);
    
        this.state = TUNNEL_STATES.CLOSED;
    
        // Close all active connections
        for (const conn of this.connections) {
          conn.destroy();
        }
        this.connections.clear();
    
        // Close server
        if (this.server) {
          this.server.close();
          this.server = null;
        }
    
        // Cancel remote forwarding if needed
        if (this.type === TUNNEL_TYPES.REMOTE) {
          this.ssh.unforwardIn(this.config.remoteHost, this.config.remotePort);
        }
    
        tunnels.delete(this.id);
      }
    
      /**
       * Reconnect tunnel
       */
      async reconnect() {
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
          logger.error(`Max reconnect attempts reached for tunnel ${this.id}`);
          this.state = TUNNEL_STATES.FAILED;
          return false;
        }
    
        this.reconnectAttempts++;
        this.state = TUNNEL_STATES.RECONNECTING;
    
        logger.info(`Reconnecting tunnel ${this.id}`, {
          attempt: this.reconnectAttempts
        });
    
        try {
          await this.start();
          this.reconnectAttempts = 0;
          return true;
        } catch (error) {
          logger.error(`Reconnect failed for tunnel ${this.id}`, {
            error: error.message
          });
    
          // Retry with exponential backoff
          const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
          setTimeout(() => this.reconnect(), delay);
    
          return false;
        }
      }
    }
  • Enumeration of tunnel states used throughout the tunnel management system.
    // Tunnel states
    export const TUNNEL_STATES = {
      CONNECTING: 'connecting',
      ACTIVE: 'active',
      RECONNECTING: 'reconnecting',
      FAILED: 'failed',
      CLOSED: 'closed'
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It states the tool creates a tunnel but lacks critical details: whether this requires authentication, if it's persistent or temporary, potential security implications, error handling, or what happens if a tunnel already exists. This is inadequate for a tool that likely involves network configuration and security.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose without unnecessary words. Every part ('Create SSH tunnel' and 'port forwarding or SOCKS proxy') contributes directly to understanding the tool's function.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 6 parameters, no annotations, and no output schema, the description is insufficient. It doesn't explain the tunnel's lifecycle, how to verify creation, what output to expect, or prerequisites like server configuration. Given the complexity of SSH tunneling and lack of structured data, more context is needed for safe and effective use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, providing clear documentation for all parameters. The description adds minimal value beyond the schema by mentioning 'port forwarding or SOCKS proxy', which loosely relates to the 'type' enum but doesn't elaborate on semantics. No additional parameter context is provided, meeting the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Create') and resource ('SSH tunnel'), with additional context about tunnel types ('port forwarding or SOCKS proxy'). It distinguishes from sibling tools like ssh_tunnel_close and ssh_tunnel_list by focusing on creation, but doesn't explicitly differentiate from other SSH tools that might involve tunneling indirectly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No explicit guidance on when to use this tool versus alternatives is provided. The description mentions tunnel types but doesn't specify scenarios for choosing between local, remote, or dynamic tunnels, or when to use this over other SSH tools like ssh_session_start for general connections.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bvisible/mcp-ssh-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server