#
tokens: 6439/50000 10/10 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── frontend
│   └── src
│       ├── index.html
│       ├── main.js
│       └── style.css
├── index.ts
├── package-lock.json
├── package.json
├── README.md
├── terminal.html
├── tsconfig.json
└── vite.config.js
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependencies
/node_modules

# Production build files
/dist

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local env files
.env*
!.env.example

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# OS files
.DS_Store
Thumbs.db
dist
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# MCP Tunnel

A simple MCP (Model Context Protocol) server that allows accessing the command line of a VM machine. When started, it also tunnels the host to the web so it can be accessed via MCP.

## Features

- Execute shell commands on a VM through MCP
- Web-based terminal interface for VM interaction
- Automatic tunneling to make the VM accessible from anywhere
- WebSocket-based real-time communication

## Prerequisites

- Node.js (v18 or newer)

## Installation and Usage

### Running with npx (no installation)

```bash
npx mcp-cli
```

### Global Installation

```bash
npm install -g mcp-cli
mcp-cli
```

### Local Development

```bash
# Clone repository
git clone [repository-url]
cd mcp-cli

# Install dependencies
npm install
```

## Development

Run the development server with hot-reloading for both backend and frontend:

```bash
npm run dev
```

## Building

Build both the frontend and backend for production:

```bash
npm run build-all
```

## Usage

1. Start the MCP server:

```bash
# Start with automatic tunneling
npm start

# Start without automatic tunneling
npm start -- --no-tunnel
```

This will build the project and start the server. By default, a tunnel will be created automatically. Use the `--no-tunnel` flag to disable automatic tunneling.

2. The server will start and provide output on stderr (to avoid interfering with MCP communication on stdout)

3. Use MCP to interact with the server using the following tools:

### Available MCP Tools

- `execute_command`: Run a shell command on the VM
  - Parameters: `{ "command": "your shell command" }`
- `start_tunnel`: Create a web tunnel to access the VM interface
  - Parameters: `{ "port": 8080, "subdomain": "optional-subdomain" }`

## Web Interface

After starting the tunnel, you can access the web-based terminal interface at the URL provided by the tunnel. This interface allows you to:

- Execute commands directly in the VM
- See command outputs in real-time
- Interact with the VM from any device with web access

## Environment Variables

Create a `.env` file to configure the server:

```
# Server configuration
PORT=8080

# Localtunnel configuration
LOCALTUNNEL_SUBDOMAIN=your-preferred-subdomain
```

## Security Considerations

This tool provides direct access to your VM's command line. Consider these security practices:

- Use strong authentication mechanisms before exposing the tunnel
- Limit the commands that can be executed through proper validation
- Consider running in a restricted environment
- Do not expose sensitive information through the tunnel

```

--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------

```javascript
import { defineConfig } from 'vite';

export default defineConfig({
  root: './frontend/src',
  publicDir: '../public',
  server: {
    port: 3000,
  },
  build: {
    outDir: '../../dist',
    emptyOutDir: true,
    sourcemap: true,
  },
});
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["index.ts"],
  "exclude": ["node_modules", "dist", "frontend"]
}

```

--------------------------------------------------------------------------------
/frontend/src/index.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>VM Terminal</title>
    <link rel="stylesheet" href="./style.css">
  </head>
  <body>
    <div class="header">
      <div>VM Terminal</div>
      <button id="clear-btn">Clear</button>
    </div>
    <div id="terminal-container"></div>
    <script type="module" src="./main.js"></script>
  </body>
</html>
```

--------------------------------------------------------------------------------
/frontend/src/style.css:
--------------------------------------------------------------------------------

```css
body {
  margin: 0;
  padding: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: #1e1e1e;
}
#terminal-container {
  flex: 1;
  padding: 10px;
  height: calc(100vh - 20px);
}
#terminal-container .terminal {
  height: 100%;
}
.header {
  background-color: #282828;
  color: #f0f0f0;
  padding: 5px 10px;
  font-family: sans-serif;
  font-size: 14px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.header button {
  background-color: #3a3a3a;
  color: #f0f0f0;
  border: none;
  padding: 5px 10px;
  border-radius: 3px;
  cursor: pointer;
}
.header button:hover {
  background-color: #4a4a4a;
}
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "mcp-cli",
  "version": "1.0.0",
  "description": "MCP server for accessing VM command line with web tunneling",
  "main": "dist/index.js",
  "type": "module",
  "bin": {
    "mcp-tunnel": "./dist/index.js"
  },
  "scripts": {
    "start": "npm run build-all && node dist/index.js",
    "build-all": "npm run build-frontend && npm run build-server",
    "build-frontend": "vite build",
    "build-server": "tsc",
    "dev": "concurrently \"ts-node index.ts\" \"vite --host\"",
    "preview": "vite preview",
    "prepare": "npm run build-all"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "@xterm/addon-fit": "^0.10.0",
    "@xterm/addon-web-links": "^0.11.0",
    "@xterm/xterm": "^5.5.0",
    "dotenv": "^16.4.1",
    "express": "^5.0.1",
    "localtunnel": "^2.0.2",
    "typescript": "^5.8.2",
    "ws": "^8.16.0",
    "zod": "^3.22.4",
    "zod-to-json-schema": "^3.22.3"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/localtunnel": "^2.0.4",
    "@types/node": "^22.13.10",
    "@types/ws": "^8.18.0",
    "@vitejs/plugin-react": "^4.3.4",
    "concurrently": "^9.1.2",
    "ts-node": "^10.9.2",
    "vite": "^6.2.2"
  },
  "publishConfig": {
    "access": "public"
  },
  "keywords": [
    "mcp",
    "tunnel",
    "terminal",
    "vm",
    "cli"
  ]
}

```

--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------

```javascript
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import '@xterm/xterm/css/xterm.css';

const term = new Terminal({
  cursorBlink: true,
  theme: {
    background: "#1e1e1e",
    foreground: "#f0f0f0",
    cursor: "#f0f0f0",
    selectionBackground: "#565656"
  },
  fontFamily: 'Menlo, Monaco, "Courier New", monospace',
  fontSize: 14,
  lineHeight: 1.2,
  scrollback: 5000,
  cursorStyle: "block"
});

// Add addons
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new WebLinksAddon());

term.open(document.getElementById("terminal-container"));
fitAddon.fit();
term.focus();

// Handle window resize
window.addEventListener("resize", () => {
  fitAddon.fit();
});

// Clear button functionality
document.getElementById("clear-btn").addEventListener("click", () => {
  term.clear();
});

let ws;
let commandBuffer = "";
let commandHistory = [];
let historyPosition = -1;

function connectWebSocket() {
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  ws = new WebSocket(`${protocol}//${window.location.host}/ws`);

  ws.onopen = () => {
    term.writeln("\r\nConnected to VM terminal");
    term.write("\r\n$ ");
  };

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === "output") {
      term.write(data.content);
      if (!data.content.endsWith("\n")) {
        term.write("\r\n");
      }
      term.write("$ ");
    }
  };

  ws.onclose = () => {
    term.writeln("\r\nConnection lost. Reconnecting...");
    setTimeout(connectWebSocket, 2000);
  };

  ws.onerror = (error) => {
    console.error("WebSocket error:", error);
    term.writeln("\r\nConnection error. Please try again.");
  };
}

function clearCurrentLine() {
  const currentLine = commandBuffer;
  for (let i = 0; i < currentLine.length; i++) {
    term.write("\b \b");
  }
  return currentLine;
}

term.onKey(({ key, domEvent }) => {
  const printable =
    !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey;

  if (domEvent.keyCode === 13) {
    // Enter key
    term.write("\r\n");

    if (commandBuffer.trim() !== "") {
      if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(
          JSON.stringify({
            type: "command",
            command: commandBuffer
          })
        );

        // Add to history if not duplicate
        if (
          commandHistory.length === 0 ||
          commandHistory[commandHistory.length - 1] !== commandBuffer
        ) {
          commandHistory.push(commandBuffer);
        }
        historyPosition = -1;
      } else {
        term.writeln("Not connected to the server.");
        term.write("$ ");
      }
    } else {
      term.write("$ ");
    }

    commandBuffer = "";
  } else if (domEvent.keyCode === 8) {
    // Backspace
    if (commandBuffer.length > 0) {
      commandBuffer = commandBuffer.slice(0, -1);
      term.write("\b \b");
    }
  } else if (domEvent.keyCode === 38) {
    // Up arrow - History previous
    if (commandHistory.length > 0) {
      if (historyPosition === -1) {
        historyPosition = commandHistory.length - 1;
      } else if (historyPosition > 0) {
        historyPosition--;
      }

      clearCurrentLine();
      commandBuffer = commandHistory[historyPosition];
      term.write(commandBuffer);
    }
  } else if (domEvent.keyCode === 40) {
    // Down arrow - History next
    if (historyPosition !== -1) {
      if (historyPosition < commandHistory.length - 1) {
        historyPosition++;
        clearCurrentLine();
        commandBuffer = commandHistory[historyPosition];
        term.write(commandBuffer);
      } else {
        historyPosition = -1;
        clearCurrentLine();
        commandBuffer = "";
      }
    }
  } else if (domEvent.ctrlKey && key.toLowerCase() === "c") {
    // Ctrl+C
    term.write("^C\r\n$ ");
    commandBuffer = "";
  } else if (domEvent.ctrlKey && key.toLowerCase() === "l") {
    // Ctrl+L (clear)
    term.clear();
    term.write("$ " + commandBuffer);
  } else if (printable) {
    commandBuffer += key;
    term.write(key);
  }
});

window.addEventListener("load", connectWebSocket);
```

--------------------------------------------------------------------------------
/terminal.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>VM Terminal</title>
    <script src="https://cdn.jsdelivr.net/npm/@xterm/[email protected]/lib/xterm.min.js"></script>
    <link
      href="https://cdn.jsdelivr.net/npm/@xterm/[email protected]/css/xterm.min.css"
      rel="stylesheet"
    />
    <script src="https://cdn.jsdelivr.net/npm/@xterm/[email protected]/lib/addon-fit.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@xterm/[email protected]/lib/addon-web-links.min.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
        height: 100vh;
        display: flex;
        flex-direction: column;
        background-color: #1e1e1e;
      }
      #terminal-container {
        flex: 1;
        padding: 10px;
        height: calc(100vh - 20px);
      }
      #terminal-container .terminal {
        height: 100%;
      }
      .header {
        background-color: #282828;
        color: #f0f0f0;
        padding: 5px 10px;
        font-family: sans-serif;
        font-size: 14px;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      .header button {
        background-color: #3a3a3a;
        color: #f0f0f0;
        border: none;
        padding: 5px 10px;
        border-radius: 3px;
        cursor: pointer;
      }
      .header button:hover {
        background-color: #4a4a4a;
      }
    </style>
  </head>
  <body>
    <div class="header">
      <div>VM Terminal</div>
      <button id="clear-btn">Clear</button>
    </div>
    <div id="terminal-container"></div>

    <script type="module">
      const term = new Terminal({
        cursorBlink: true,
        theme: {
          background: "#1e1e1e",
          foreground: "#f0f0f0",
          cursor: "#f0f0f0",
          selectionBackground: "#565656"
        },
        fontFamily: 'Menlo, Monaco, "Courier New", monospace',
        fontSize: 14,
        lineHeight: 1.2,
        scrollback: 5000,
        cursorStyle: "block"
      });

      // Add addons
      const fitAddon = new FitAddon();
      term.loadAddon(fitAddon);
      term.loadAddon(new WebLinksAddon());

      term.open(document.getElementById("terminal-container"));
      fitAddon.fit();
      term.focus();

      // Handle window resize
      window.addEventListener("resize", () => {
        fitAddon.fit();
      });

      // Clear button functionality
      document.getElementById("clear-btn").addEventListener("click", () => {
        term.clear();
      });

      let ws;
      let commandBuffer = "";
      let commandHistory = [];
      let historyPosition = -1;

      function connectWebSocket() {
        ws = new WebSocket("ws://" + window.location.host);

        ws.onopen = () => {
          term.writeln("\r\nConnected to VM terminal");
          term.write("\r\n$ ");
        };

        ws.onmessage = (event) => {
          const data = JSON.parse(event.data);
          if (data.type === "output") {
            term.write(data.content);
            if (!data.content.endsWith("\n")) {
              term.write("\r\n");
            }
            term.write("$ ");
          }
        };

        ws.onclose = () => {
          term.writeln("\r\nConnection lost. Reconnecting...");
          setTimeout(connectWebSocket, 2000);
        };

        ws.onerror = (error) => {
          console.error("WebSocket error:", error);
          term.writeln("\r\nConnection error. Please try again.");
        };
      }

      function clearCurrentLine() {
        const currentLine = commandBuffer;
        for (let i = 0; i < currentLine.length; i++) {
          term.write("\b \b");
        }
        return currentLine;
      }

      term.onKey(({ key, domEvent }) => {
        const printable =
          !domEvent.altKey && !domEvent.ctrlKey && !domEvent.metaKey;

        if (domEvent.keyCode === 13) {
          // Enter key
          term.write("\r\n");

          if (commandBuffer.trim() !== "") {
            if (ws && ws.readyState === WebSocket.OPEN) {
              ws.send(
                JSON.stringify({
                  type: "command",
                  command: commandBuffer
                })
              );

              // Add to history if not duplicate
              if (
                commandHistory.length === 0 ||
                commandHistory[commandHistory.length - 1] !== commandBuffer
              ) {
                commandHistory.push(commandBuffer);
              }
              historyPosition = -1;
            } else {
              term.writeln("Not connected to the server.");
              term.write("$ ");
            }
          } else {
            term.write("$ ");
          }

          commandBuffer = "";
        } else if (domEvent.keyCode === 8) {
          // Backspace
          if (commandBuffer.length > 0) {
            commandBuffer = commandBuffer.slice(0, -1);
            term.write("\b \b");
          }
        } else if (domEvent.keyCode === 38) {
          // Up arrow - History previous
          if (commandHistory.length > 0) {
            if (historyPosition === -1) {
              historyPosition = commandHistory.length - 1;
            } else if (historyPosition > 0) {
              historyPosition--;
            }

            clearCurrentLine();
            commandBuffer = commandHistory[historyPosition];
            term.write(commandBuffer);
          }
        } else if (domEvent.keyCode === 40) {
          // Down arrow - History next
          if (historyPosition !== -1) {
            if (historyPosition < commandHistory.length - 1) {
              historyPosition++;
              clearCurrentLine();
              commandBuffer = commandHistory[historyPosition];
              term.write(commandBuffer);
            } else {
              historyPosition = -1;
              clearCurrentLine();
              commandBuffer = "";
            }
          }
        } else if (domEvent.ctrlKey && key.toLowerCase() === "c") {
          // Ctrl+C
          term.write("^C\r\n$ ");
          commandBuffer = "";
        } else if (domEvent.ctrlKey && key.toLowerCase() === "l") {
          // Ctrl+L (clear)
          term.clear();
          term.write("$ " + commandBuffer);
        } else if (printable) {
          commandBuffer += key;
          term.write(key);
        }
      });

      window.addEventListener("load", connectWebSocket);
    </script>
  </body>
</html>

```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ErrorCode,
  McpError
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { spawn } from "child_process";
import { createServer } from "http";
import { WebSocketServer } from "ws";
import { createReadStream, existsSync } from "fs";
import { join } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import dotenv from "dotenv";
import localtunnel from "localtunnel";
import express from "express";

dotenv.config();

// Schemas for our tools
const shellCommandSchema = z.object({
  command: z.string().describe("Shell command to execute on the VM")
});

const tunnelConfigSchema = z.object({
  port: z.number().default(8080).describe("Port to tunnel to the web"),
  subdomain: z.string().optional().describe("Optional subdomain for the tunnel")
});

class VmMcpServer {
  private server: Server;
  private webServer: any;
  private wss!: WebSocketServer;
  private tunnel: any;
  private tunnelUrl: string | undefined;
  private serverPort = 8080;
  private __dirname = dirname(fileURLToPath(import.meta.url));
  private noTunnel = process.argv.includes("--no-tunnel");
  private app: any;
  private transport: any;

  constructor() {
    this.server = new Server(
      {
        name: "vm-mcp-server",
        version: "0.1.0"
      },
      {
        capabilities: {
          resources: {},
          tools: {}
        }
      }
    );

    this.setupHandlers();
    this.setupErrorHandling();
    this.setupWebServer();
  }

  private setupWebServer() {
    this.app = express();
    const distPath = join(this.__dirname, "/");
    const devPath = join(this.__dirname, "frontend", "src");

    // Check if we're in production (using built files) or development
    const isProduction = existsSync(distPath);
    const staticPath = isProduction ? distPath : devPath;

    // Serve static files
    this.app.use(express.static(staticPath));

    // Fallback route for SPA
    this.app.get("/", (req: any, res: any) => {
      res.sendFile(join(staticPath, "index.html"));
    });

    // Create HTTP server from Express app
    this.webServer = createServer(this.app);

    // Create WebSocket server for real-time communication
    this.wss = new WebSocketServer({
      server: this.webServer,
      path: "/ws"
    });

    this.wss.on("connection", (ws) => {
      console.error("Client connected to WebSocket");

      ws.on("message", (message) => {
        try {
          const data = JSON.parse(message.toString());
          if (data.type === "command") {
            this.executeCommand(data.command, (output) => {
              ws.send(JSON.stringify({ type: "output", content: output }));
            });
          }
        } catch (error) {
          console.error("Error processing WebSocket message:", error);
        }
      });
    });
  }

  private executeCommand(command: string, callback: (output: string) => void) {
    console.error(`Executing command: ${command}`);

    const process = spawn("bash", ["-c", command]);

    process.stdout.on("data", (data: Buffer) => {
      callback(data.toString());
    });

    process.stderr.on("data", (data: Buffer) => {
      callback(data.toString());
    });

    process.on("error", (error: Error) => {
      callback(`Error: ${error.message}`);
    });

    process.on("close", (code: number | null) => {
      callback(`Command exited with code ${code}`);
    });
  }

  private setupErrorHandling() {
    this.server.onerror = (error) => {
      console.error("[MCP Error]", error);
    };

    process.on("SIGINT", async () => {
      if (this.tunnel) {
        this.tunnel.close();
      }
      await this.server.close();
      this.webServer.close();
      process.exit(0);
    });
  }

  private setupHandlers() {
    this.setupToolHandlers();
  }

  private setupToolHandlers() {
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: "execute_command",
          description: "Execute a shell command on the VM",
          inputSchema: zodToJsonSchema(shellCommandSchema)
        },
        {
          name: "start_tunnel",
          description: "Start a web tunnel to access the VM interface",
          inputSchema: zodToJsonSchema(tunnelConfigSchema)
        }
      ]
    }));

    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      switch (request.params.name) {
        case "execute_command":
          return this.handleExecuteCommand(request);
        case "start_tunnel":
          return this.handleStartTunnel(request);
        default:
          throw new McpError(
            ErrorCode.MethodNotFound,
            `Unknown tool: ${request.params.name}`
          );
      }
    });
  }

  private async handleExecuteCommand(request: any) {
    const parsed = shellCommandSchema.safeParse(request.params.arguments);
    if (!parsed.success) {
      throw new McpError(ErrorCode.InvalidParams, "Invalid command arguments");
    }

    const { command } = parsed.data;

    return new Promise<any>((resolve) => {
      let output = "";

      this.executeCommand(command, (data) => {
        output += data;
      });

      // Simple timeout to collect output
      setTimeout(() => {
        resolve({
          content: [
            {
              type: "text",
              text:
                output ||
                "Command executed (no output or still running in background)"
            }
          ]
        });
      }, 2000);
    });
  }

  private async handleStartTunnel(request: any) {
    const parsed = tunnelConfigSchema.safeParse(request.params.arguments);
    if (!parsed.success) {
      throw new McpError(
        ErrorCode.InvalidParams,
        "Invalid tunnel configuration"
      );
    }

    const { port, subdomain } = parsed.data;
    this.serverPort = port;

    // Close existing tunnel if any
    if (this.tunnel) {
      this.tunnel.close();
    }

    try {
      // Create the tunnel
      const tunnelOptions: any = {
        port: this.serverPort
      };

      if (subdomain) {
        tunnelOptions.subdomain = subdomain;
      }

      this.tunnel = await localtunnel(tunnelOptions);
      this.tunnelUrl = this.tunnel.url;

      return {
        content: [
          {
            type: "text",
            text: `Tunnel created successfully. VM interface available at: ${this.tunnelUrl}`
          }
        ]
      };
    } catch (error: any) {
      throw new McpError(
        ErrorCode.InternalError,
        `Failed to create tunnel: ${error.message || String(error)}`
      );
    }
  }

  mcpTransportStart = async () => {
    this.app.get("/sse", async (req: any, res: any) => {
      this.transport = new SSEServerTransport("/messages", res);
      await this.server.connect(this.transport);
    });

    this.app.post("/messages", async (req: any, res: any) => {
      // Note: to support multiple simultaneous connections, these messages will
      // need to be routed to a specific matching transport. (This logic isn't
      // implemented here, for simplicity.)
      await this.transport.handlePostMessage(req, res);
    });
  };

  async run() {
    // Start the MCP server
    await this.mcpTransportStart();
    // Start the web server
    this.webServer.listen(this.serverPort, async () => {
      console.log(
        `Web server running on port http://localhost:${this.serverPort}`
      );

      // Auto-start tunnel unless --no-tunnel flag is provided
      if (!this.noTunnel) {
        await this.startTunnelOnBoot().catch((err) => {
          console.error("Failed to start tunnel on boot:", err.message);
        });
      }
    });

    console.error("VM MCP server running on stdio");
  }

  private async startTunnelOnBoot() {
    try {
      // Create the tunnel
      const tunnelOptions: any = {
        port: this.serverPort
      };

      // Optional subdomain from environment variable
      const subdomain = process.env.LOCALTUNNEL_SUBDOMAIN;
      if (subdomain) {
        tunnelOptions.subdomain = subdomain;
      }

      this.tunnel = await localtunnel(tunnelOptions);
      this.tunnelUrl = this.tunnel.url;

      console.error(
        `Tunnel created automatically. VM interface available at: ${this.tunnelUrl}`
      );

      return this.tunnelUrl;
    } catch (error: any) {
      console.error(
        `Failed to create tunnel: ${error.message || String(error)}`
      );
      throw error;
    }
  }
}

const server = new VmMcpServer();
server.run().catch(console.error);

```