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

```
├── .gitignore
├── config.sample.json
├── Dockerfile
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── index.ts
│   ├── types
│   │   └── config.ts
│   └── utils
│       ├── config.ts
│       ├── ssh.ts
│       ├── sshManager.ts
│       └── validation.ts
├── tests
│   └── validation.test.ts
└── tsconfig.json
```

# Files

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

```
node_modules/
dist/
.env
.DS_Store
*.log
config.json
.cursorrules
.history
CLAUDE.md

```

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

```markdown
# Windows CLI MCP Server

> [!CAUTION]
> **PROJECT DEPRECATED** - No longer maintained.
> Use https://github.com/wonderwhy-er/DesktopCommanderMCP instead for similar functionality.

[![NPM Downloads](https://img.shields.io/npm/dt/@simonb97/server-win-cli.svg?style=flat)](https://www.npmjs.com/package/@simonb97/server-win-cli)
[![NPM Version](https://img.shields.io/npm/v/@simonb97/server-win-cli.svg?style=flat)](https://www.npmjs.com/package/@simonb97/server-win-cli?activeTab=versions)
[![smithery badge](https://smithery.ai/badge/@simonb97/server-win-cli)](https://smithery.ai/server/@simonb97/server-win-cli)

[MCP server](https://modelcontextprotocol.io/introduction) for secure command-line interactions on Windows systems, enabling controlled access to PowerShell, CMD, Git Bash shells, and remote systems via SSH. It allows MCP clients (like [Claude Desktop](https://claude.ai/download)) to perform operations on your system, similar to [Open Interpreter](https://github.com/OpenInterpreter/open-interpreter).

>[!IMPORTANT]
> This MCP server provides direct access to your system's command line interface and remote systems via SSH. When enabled, it grants access to your files, environment variables, command execution capabilities, and remote server management.
>
> - Review and restrict allowed paths and SSH connections
> - Enable directory restrictions
> - Configure command blocks
> - Consider security implications
>
> See [Configuration](#configuration) for more details.

- [Features](#features)
- [Usage with Claude Desktop](#usage-with-claude-desktop)
- [Configuration](#configuration)
  - [Configuration Locations](#configuration-locations)
  - [Default Configuration](#default-configuration)
  - [Configuration Settings](#configuration-settings)
    - [Security Settings](#security-settings)
    - [Shell Configuration](#shell-configuration)
    - [SSH Configuration](#ssh-configuration)
- [API](#api)
  - [Tools](#tools)
  - [Resources](#resources)
- [Security Considerations](#security-considerations)
- [License](#license)

## Features

- **Multi-Shell Support**: Execute commands in PowerShell, Command Prompt (CMD), and Git Bash
- **SSH Support**: Execute commands on remote systems via SSH
- **Resource Exposure**: View SSH connections, current directory, and configuration as MCP resources
- **Security Controls**:
  - Command and SSH command blocking (full paths, case variations)
  - Working directory validation
  - Maximum command length limits
  - Command logging and history tracking
  - Smart argument validation
- **Configurable**:
  - Custom security rules
  - Shell-specific settings
  - SSH connection profiles
  - Path restrictions
  - Blocked command lists

See the [API](#api) section for more details on the tools and resources the server provides to MCP clients.

**Note**: The server will only allow operations within configured directories, with allowed commands, and on configured SSH connections.

## Usage with Claude Desktop

Add this to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "windows-cli": {
      "command": "npx",
      "args": ["-y", "@simonb97/server-win-cli"]
    }
  }
}
```

For use with a specific config file, add the `--config` flag:

```json
{
  "mcpServers": {
    "windows-cli": {
      "command": "npx",
      "args": [
        "-y",
        "@simonb97/server-win-cli",
        "--config",
        "path/to/your/config.json"
      ]
    }
  }
}
```

After configuring, you can:
- Execute commands directly using the available tools
- View configured SSH connections and server configuration in the Resources section
- Manage SSH connections through the provided tools

## Configuration

The server uses a JSON configuration file to customize its behavior. You can specify settings for security controls, shell configurations, and SSH connections.

1. To create a default config file, either:

**a)** copy `config.json.example` to `config.json`, or

**b)** run:

```bash
npx @simonb97/server-win-cli --init-config ./config.json
```

2. Then set the `--config` flag to point to your config file as described in the [Usage with Claude Desktop](#usage-with-claude-desktop) section.

### Configuration Locations

The server looks for configuration in the following locations (in order):

1. Path specified by `--config` flag
2. ./config.json in current directory
3. ~/.win-cli-mcp/config.json in user's home directory

If no configuration file is found, the server will use a default (restricted) configuration:

### Default Configuration

**Note**: The default configuration is designed to be restrictive and secure. Find more details on each setting in the [Configuration Settings](#configuration-settings) section.

```json
{
  "security": {
    "maxCommandLength": 2000,
    "blockedCommands": [
      "rm",
      "del",
      "rmdir",
      "format",
      "shutdown",
      "restart",
      "reg",
      "regedit",
      "net",
      "netsh",
      "takeown",
      "icacls"
    ],
    "blockedArguments": [
      "--exec",
      "-e",
      "/c",
      "-enc",
      "-encodedcommand",
      "-command",
      "--interactive",
      "-i",
      "--login",
      "--system"
    ],
    "allowedPaths": ["User's home directory", "Current working directory"],
    "restrictWorkingDirectory": true,
    "logCommands": true,
    "maxHistorySize": 1000,
    "commandTimeout": 30,
    "enableInjectionProtection": true
  },
  "shells": {
    "powershell": {
      "enabled": true,
      "command": "powershell.exe",
      "args": ["-NoProfile", "-NonInteractive", "-Command"],
      "blockedOperators": ["&", "|", ";", "`"]
    },
    "cmd": {
      "enabled": true,
      "command": "cmd.exe",
      "args": ["/c"],
      "blockedOperators": ["&", "|", ";", "`"]
    },
    "gitbash": {
      "enabled": true,
      "command": "C:\\Program Files\\Git\\bin\\bash.exe",
      "args": ["-c"],
      "blockedOperators": ["&", "|", ";", "`"]
    }
  },
  "ssh": {
    "enabled": false,
    "defaultTimeout": 30,
    "maxConcurrentSessions": 5,
    "keepaliveInterval": 10000,
    "keepaliveCountMax": 3,
    "readyTimeout": 20000,
    "connections": {}
  }
}
```

### Configuration Settings

The configuration file is divided into three main sections: `security`, `shells`, and `ssh`.

#### Security Settings

```json
{
  "security": {
    // Maximum allowed length for any command
    "maxCommandLength": 1000,

    // Commands to block - blocks both direct use and full paths
    // Example: "rm" blocks both "rm" and "C:\\Windows\\System32\\rm.exe"
    // Case-insensitive: "del" blocks "DEL.EXE", "del.cmd", etc.
    "blockedCommands": [
      "rm", // Delete files
      "del", // Delete files
      "rmdir", // Delete directories
      "format", // Format disks
      "shutdown", // Shutdown system
      "restart", // Restart system
      "reg", // Registry editor
      "regedit", // Registry editor
      "net", // Network commands
      "netsh", // Network commands
      "takeown", // Take ownership of files
      "icacls" // Change file permissions
    ],

    // Arguments that will be blocked when used with any command
    // Note: Checks each argument independently - "cd warm_dir" won't be blocked just because "rm" is in blockedCommands
    "blockedArguments": [
      "--exec", // Execution flags
      "-e", // Short execution flags
      "/c", // Command execution in some shells
      "-enc", // PowerShell encoded commands
      "-encodedcommand", // PowerShell encoded commands
      "-command", // Direct PowerShell command execution
      "--interactive", // Interactive mode which might bypass restrictions
      "-i", // Short form of interactive
      "--login", // Login shells might have different permissions
      "--system" // System level operations
    ],

    // List of directories where commands can be executed
    "allowedPaths": ["C:\\Users\\YourUsername", "C:\\Projects"],

    // If true, commands can only run in allowedPaths
    "restrictWorkingDirectory": true,

    // If true, saves command history
    "logCommands": true,

    // Maximum number of commands to keep in history
    "maxHistorySize": 1000,

    // Timeout for command execution in seconds (default: 30)
    "commandTimeout": 30,

    // Enable or disable protection against command injection (covers ;, &, |, \`)
    "enableInjectionProtection": true
  }
}
```

#### Shell Configuration

```json
{
  "shells": {
    "powershell": {
      // Enable/disable this shell
      "enabled": true,
      // Path to shell executable
      "command": "powershell.exe",
      // Default arguments for the shell
      "args": ["-NoProfile", "-NonInteractive", "-Command"],
      // Optional: Specify which command operators to block
      "blockedOperators": ["&", "|", ";", "`"]  // Block all command chaining
    },
    "cmd": {
      "enabled": true,
      "command": "cmd.exe",
      "args": ["/c"],
      "blockedOperators": ["&", "|", ";", "`"]  // Block all command chaining
    },
    "gitbash": {
      "enabled": true,
      "command": "C:\\Program Files\\Git\\bin\\bash.exe",
      "args": ["-c"],
      "blockedOperators": ["&", "|", ";", "`"]  // Block all command chaining
    }
  }
}
```

#### SSH Configuration

```json
{
  "ssh": {
    // Enable/disable SSH functionality
    "enabled": false,

    // Default timeout for SSH commands in seconds
    "defaultTimeout": 30,

    // Maximum number of concurrent SSH sessions
    "maxConcurrentSessions": 5,

    // Interval for sending keepalive packets (in milliseconds)
    "keepaliveInterval": 10000,

    // Maximum number of failed keepalive attempts before disconnecting
    "keepaliveCountMax": 3,

    // Timeout for establishing SSH connections (in milliseconds)
    "readyTimeout": 20000,

    // SSH connection profiles
    "connections": {
      // NOTE: these examples are not set in the default config!
      // Example: Local Raspberry Pi
      "raspberry-pi": {
        "host": "raspberrypi.local", // Hostname or IP address
        "port": 22, // SSH port
        "username": "pi", // SSH username
        "password": "raspberry", // Password authentication (if not using key)
        "keepaliveInterval": 10000, // Override global keepaliveInterval
        "keepaliveCountMax": 3, // Override global keepaliveCountMax
        "readyTimeout": 20000 // Override global readyTimeout
      },
      // Example: Remote server with key authentication
      "dev-server": {
        "host": "dev.example.com",
        "port": 22,
        "username": "admin",
        "privateKeyPath": "C:\\Users\\YourUsername\\.ssh\\id_rsa", // Path to private key
        "keepaliveInterval": 10000,
        "keepaliveCountMax": 3,
        "readyTimeout": 20000
      }
    }
  }
}
```

## API

### Tools

- **execute_command**

  - Execute a command in the specified shell
  - Inputs:
    - `shell` (string): Shell to use ("powershell", "cmd", or "gitbash")
    - `command` (string): Command to execute
    - `workingDir` (optional string): Working directory
  - Returns command output as text, or error message if execution fails

- **get_command_history**

  - Get the history of executed commands
  - Input: `limit` (optional number)
  - Returns timestamped command history with outputs

- **ssh_execute**

  - Execute a command on a remote system via SSH
  - Inputs:
    - `connectionId` (string): ID of the SSH connection to use
    - `command` (string): Command to execute
  - Returns command output as text, or error message if execution fails

- **ssh_disconnect**
  - Disconnect from an SSH server
  - Input:
    - `connectionId` (string): ID of the SSH connection to disconnect
  - Returns confirmation message

- **create_ssh_connection**
  - Create a new SSH connection
  - Inputs:
    - `connectionId` (string): ID for the new SSH connection
    - `connectionConfig` (object): Connection configuration details including host, port, username, and either password or privateKeyPath
  - Returns confirmation message

- **read_ssh_connections**
  - Read all configured SSH connections
  - Returns a list of all SSH connections from the configuration

- **update_ssh_connection**
  - Update an existing SSH connection
  - Inputs:
    - `connectionId` (string): ID of the SSH connection to update
    - `connectionConfig` (object): New connection configuration details
  - Returns confirmation message

- **delete_ssh_connection**
  - Delete an SSH connection
  - Input:
    - `connectionId` (string): ID of the SSH connection to delete
  - Returns confirmation message

- **get_current_directory**
  - Get the current working directory of the server
  - Returns the current working directory path

### Resources

- **SSH Connections**
  - URI format: `ssh://{connectionId}`
  - Contains connection details with sensitive information masked
  - One resource for each configured SSH connection
  - Example: `ssh://raspberry-pi` shows configuration for the "raspberry-pi" connection

- **SSH Configuration**
  - URI: `ssh://config`
  - Contains overall SSH configuration and all connections (with passwords masked)
  - Shows settings like defaultTimeout, maxConcurrentSessions, and the list of connections

- **Current Directory**
  - URI: `cli://currentdir`
  - Contains the current working directory of the CLI server
  - Shows the path where commands will execute by default

- **CLI Configuration**
  - URI: `cli://config`
  - Contains the CLI server configuration (excluding sensitive data)
  - Shows security settings, shell configurations, and SSH settings

## Security Considerations

### Built-in Security Features (Always Active)

The following security features are hard-coded into the server and cannot be disabled:

- **Case-insensitive command blocking**: All command blocking is case-insensitive (e.g., "DEL.EXE", "del.cmd", etc. are all blocked if "del" is in blockedCommands)
- **Smart path parsing**: The server parses full command paths to prevent bypass attempts (blocking "C:\\Windows\\System32\\rm.exe" if "rm" is blocked)
- **Command parsing intelligence**: False positives are avoided (e.g., "warm_dir" is not blocked just because "rm" is in blockedCommands)
- **Input validation**: All user inputs are validated before execution
- **Shell process management**: Processes are properly terminated after execution or timeout
- **Sensitive data masking**: Passwords are automatically masked in resources (replaced with ********)

### Configurable Security Features (Active by Default)

These security features are configurable through the config.json file:

- **Command blocking**: Commands specified in `blockedCommands` array are blocked (default includes dangerous commands like rm, del, format)
- **Argument blocking**: Arguments specified in `blockedArguments` array are blocked (default includes potentially dangerous flags)
- **Command injection protection**: Prevents command chaining (enabled by default through `enableInjectionProtection: true`)
- **Working directory restriction**: Limits command execution to specified directories (enabled by default through `restrictWorkingDirectory: true`)
- **Command length limit**: Restricts maximum command length (default: 2000 characters)
- **Command timeout**: Terminates commands that run too long (default: 30 seconds)
- **Command logging**: Records command history (enabled by default through `logCommands: true`)

### Important Security Warnings

These are not features but important security considerations to be aware of:

- **Environment access**: Commands may have access to environment variables, which could contain sensitive information
- **File system access**: Commands can read/write files within allowed paths - carefully configure `allowedPaths` to prevent access to sensitive data

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

```

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

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": [
    "src/**/*"
  ]
}
```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
export default {
  preset: 'ts-jest',
  testEnvironment: 'node',
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        useESM: true,
      },
    ],
  },
};

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - configPath
    properties:
      configPath:
        type: string
        description: Path to the configuration file for the Windows CLI MCP server.
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    (config) => ({ command: 'node', args: ['dist/index.js', '--config', config.configPath] })

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Use an official Node.js runtime as a parent image
FROM node:18-alpine AS build

# Set the working directory in the container
WORKDIR /app

# Copy the package files and install the dependencies
COPY package.json package-lock.json ./
RUN npm install --ignore-scripts

# Copy the rest of the application
COPY . .

# Build the TypeScript code
RUN npm run build

# Use a lighter image for the runtime
FROM node:18-alpine AS runtime

WORKDIR /app

# Copy the built application from the build stage
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/package-lock.json ./package-lock.json

# Install only production dependencies
RUN npm ci --omit=dev

# Command to run the application
ENTRYPOINT ["node", "dist/index.js"]

```

--------------------------------------------------------------------------------
/src/types/config.ts:
--------------------------------------------------------------------------------

```typescript
export interface SecurityConfig {
  maxCommandLength: number;
  blockedCommands: string[];
  blockedArguments: string[];
  allowedPaths: string[];
  restrictWorkingDirectory: boolean;
  logCommands: boolean;
  maxHistorySize: number;
  commandTimeout: number;
  enableInjectionProtection: boolean;
}

export interface ShellConfig {
  enabled: boolean;
  command: string;
  args: string[];
  validatePath?: (dir: string) => boolean;
  blockedOperators?: string[]; // Added for shell-specific operator restrictions
}

export interface SSHConnectionConfig {
  host: string;
  port: number;
  username: string;
  privateKeyPath?: string;
  password?: string;
  keepaliveInterval?: number;
  keepaliveCountMax?: number;
  readyTimeout?: number;
}

export interface SSHConfig {
  enabled: boolean;
  connections: Record<string, SSHConnectionConfig>;
  defaultTimeout: number;
  maxConcurrentSessions: number;
  keepaliveInterval: number;
  keepaliveCountMax: number;
  readyTimeout: number;
}

export interface ServerConfig {
  security: SecurityConfig;
  shells: {
    powershell: ShellConfig;
    cmd: ShellConfig;
    gitbash: ShellConfig;
  };
  ssh: SSHConfig;
}

export interface CommandHistoryEntry {
  command: string;
  output: string;
  timestamp: string;
  exitCode: number;
  connectionId?: string;
}
```

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

```json
{
  "name": "@simonb97/server-win-cli",
  "version": "0.2.1",
  "description": "MCP server for Windows CLI interactions",
  "type": "module",
  "bin": {
    "server-win-cli": "./dist/index.js"
  },
  "files": [
    "dist"
  ],
  "license": "MIT",
  "author": "Simon Benedict",
  "homepage": "https://github.com/SimonB97/win-cli-mcp-server",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/SimonB97/win-cli-mcp-server.git"
  },
  "bugs": {
    "url": "https://github.com/SimonB97/win-cli-mcp-server/issues"
  },
  "keywords": [
    "mcp",
    "claude",
    "cli",
    "windows",
    "modelcontextprotocol",
    "mcp-server",
    "ssh"
  ],
  "engines": {
    "node": ">=18.0.0"
  },
  "scripts": {
    "build": "tsc && shx chmod +x dist/index.js",
    "prepare": "npm run build",
    "watch": "tsc --watch",
    "start": "node dist/index.js",
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
    "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
    "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.0.1",
    "@types/ssh2": "^1.15.1",
    "ssh2": "^1.16.0",
    "yargs": "^17.7.2",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@jest/globals": "^29.7.0",
    "@types/jest": "^29.5.11",
    "@types/node": "^20.11.0",
    "@types/yargs": "^17.0.33",
    "jest": "^29.7.0",
    "shx": "^0.3.4",
    "ts-jest": "^29.1.1",
    "typescript": "^5.3.3"
  }
}

```

--------------------------------------------------------------------------------
/config.sample.json:
--------------------------------------------------------------------------------

```json
{
  "security": {
    "maxCommandLength": 1000,
    "blockedCommands": [
      "rm",
      "del",
      "rmdir",
      "format",
      "shutdown",
      "restart",
      "reg",
      "regedit",
      "net",
      "netsh",
      "takeown",
      "icacls"
    ],
    "blockedArguments": ["-rf", "/f", "/s", "/q"],
    "allowedPaths": ["C:\\Users\\YourUsername", "C:\\Projects"],
    "restrictWorkingDirectory": true,
    "logCommands": true,
    "maxHistorySize": 1000,
    "commandTimeout": 30,
    "enableInjectionProtection": true
  },
  "shells": {
    "powershell": {
      "enabled": true,
      "command": "powershell.exe",
      "args": ["-NoProfile", "-NonInteractive", "-Command"],
      "blockedOperators": ["&", "|", ";", "`"]
    },
    "cmd": {
      "enabled": true,
      "command": "cmd.exe",
      "args": ["/c"],
      "blockedOperators": ["&", "|", ";", "`"]
    },
    "gitbash": {
      "enabled": true,
      "command": "C:\\Program Files\\Git\\bin\\bash.exe",
      "args": ["-c"],
      "blockedOperators": ["&", "|", ";", "`"]
    }
  },
  "ssh": {
    "enabled": false,
    "defaultTimeout": 30,
    "maxConcurrentSessions": 5,
    "keepaliveInterval": 10000,
    "readyTimeout": 20000,
    "connections": {
      "raspberry-pi": {
        "host": "raspberrypi.local",
        "port": 22,
        "username": "pi",
        "password": "raspberry",
        "keepaliveInterval": 10000,
        "readyTimeout": 20000
      },
      "dev-server": {
        "host": "dev.example.com",
        "port": 22,
        "username": "admin",
        "privateKeyPath": "C:\\Users\\YourUsername\\.ssh\\id_rsa",
        "keepaliveInterval": 10000,
        "readyTimeout": 20000
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/src/utils/sshManager.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs';
import path from 'path';
import { ServerConfig } from '../types/config.js';
import { loadConfig as loadMainConfig } from './config.js';

/**
 * Load the current configuration from the config file.
 */
const loadConfig = (): ServerConfig => {
  try {
    // Use the same config file that the main application uses
    return loadMainConfig();
  } catch (error) {
    console.error('Error loading configuration:', error);
    throw error;
  }
};

/**
 * Save the updated configuration to the config file.
 * @param config The updated configuration object.
 */
const saveConfig = (config: ServerConfig): void => {
  try {
    // Use the actual config path from the process args or default
    const args = process.argv.slice(2);
    let configPath = './config.json';
    
    // Try to find a config path in the arguments
    for (let i = 0; i < args.length - 1; i++) {
      if ((args[i] === '--config' || args[i] === '-c') && args[i + 1]) {
        configPath = args[i + 1];
        break;
      }
    }
    
    // Resolve the path to be safe
    const resolvedPath = path.resolve(configPath);
    fs.writeFileSync(resolvedPath, JSON.stringify(config, null, 2));
  } catch (error) {
    console.error('Error saving configuration:', error);
    throw error;
  }
};

/**
 * Create a new SSH connection.
 * @param connectionId The ID for the new connection.
 * @param connectionConfig The configuration for the new connection.
 */
const createSSHConnection = (connectionId: string, connectionConfig: any): void => {
  const config = loadConfig();
  config.ssh.connections[connectionId] = connectionConfig;
  saveConfig(config);
};

/**
 * Read all SSH connections.
 * @returns An object containing all SSH connections.
 */
const readSSHConnections = (): object => {
  const config = loadConfig();
  return config.ssh.connections;
};

/**
 * Update an existing SSH connection.
 * @param connectionId The ID of the connection to update.
 * @param connectionConfig The new configuration for the connection.
 */
const updateSSHConnection = (connectionId: string, connectionConfig: any): void => {
  const config = loadConfig();
  if (config.ssh.connections[connectionId]) {
    config.ssh.connections[connectionId] = connectionConfig;
    saveConfig(config);
  }
};

/**
 * Delete an SSH connection.
 * @param connectionId The ID of the connection to delete.
 */
const deleteSSHConnection = (connectionId: string): void => {
  const config = loadConfig();
  delete config.ssh.connections[connectionId];
  saveConfig(config);
};

export { createSSHConnection, readSSHConnections, updateSSHConnection, deleteSSHConnection }; 
```

--------------------------------------------------------------------------------
/src/utils/ssh.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from 'ssh2';
import { SSHConnectionConfig } from '../types/config.js';
import fs from 'fs/promises';

export class SSHConnection {
  private client: Client;
  private config: SSHConnectionConfig;
  private isConnected: boolean = false;
  private reconnectTimer: NodeJS.Timeout | null = null;
  private lastActivity: number = Date.now();

  constructor(config: SSHConnectionConfig) {
    this.client = new Client();
    this.config = config;
    this.setupClientEvents();
  }

  private setupClientEvents() {
    this.client
      .on('error', (err) => {
        console.error(`SSH connection error for ${this.config.host}:`, err.message);
        this.isConnected = false;
        this.scheduleReconnect();
      })
      .on('end', () => {
        console.error(`SSH connection ended for ${this.config.host}`);
        this.isConnected = false;
        this.scheduleReconnect();
      })
      .on('close', () => {
        console.error(`SSH connection closed for ${this.config.host}`);
        this.isConnected = false;
        this.scheduleReconnect();
      });
  }

  private scheduleReconnect() {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
    }

    // Only attempt reconnect if there was recent activity
    const timeSinceLastActivity = Date.now() - this.lastActivity;
    if (timeSinceLastActivity < 30 * 60 * 1000) { // 30 minutes
      this.reconnectTimer = setTimeout(() => {
        console.error(`Attempting to reconnect to ${this.config.host}...`);
        this.connect().catch(err => {
          console.error(`Reconnection failed for ${this.config.host}:`, err.message);
        });
      }, 5000); // Wait 5 seconds before reconnecting
    }
  }

  async connect(): Promise<void> {
    if (this.isConnected) {
      return;
    }

    return new Promise(async (resolve, reject) => {
      try {
        const connectionConfig: any = {
          host: this.config.host,
          port: this.config.port,
          username: this.config.username,
          keepaliveInterval: this.config.keepaliveInterval || 10000,
          keepaliveCountMax: this.config.keepaliveCountMax || 3,
          readyTimeout: this.config.readyTimeout || 20000,
        };

        // Handle authentication
        if (this.config.privateKeyPath) {
          const privateKey = await fs.readFile(this.config.privateKeyPath, 'utf8');
          connectionConfig.privateKey = privateKey;
        } else if (this.config.password) {
          connectionConfig.password = this.config.password;
        } else {
          throw new Error('No authentication method provided');
        }

        this.client
          .on('ready', () => {
            this.isConnected = true;
            this.lastActivity = Date.now();
            resolve();
          })
          .on('error', (err) => {
            reject(err);
          })
          .connect(connectionConfig);
      } catch (error) {
        reject(error);
      }
    });
  }

  async executeCommand(command: string): Promise<{ output: string; exitCode: number }> {
    this.lastActivity = Date.now();

    // Check connection and attempt reconnect if needed
    if (!this.isConnected) {
      await this.connect();
    }

    return new Promise((resolve, reject) => {
      this.client.exec(command, (err, stream) => {
        if (err) {
          reject(err);
          return;
        }

        let output = '';
        let errorOutput = '';

        stream
          .on('data', (data: Buffer) => {
            output += data.toString();
          })
          .stderr.on('data', (data: Buffer) => {
            errorOutput += data.toString();
          });

        stream.on('close', (code: number) => {
          this.lastActivity = Date.now();
          resolve({
            output: output || errorOutput,
            exitCode: code || 0
          });
        });
      });
    });
  }

  disconnect(): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    
    if (this.isConnected) {
      this.client.end();
      this.isConnected = false;
    }
  }

  isActive(): boolean {
    return this.isConnected;
  }
}

// Connection pool to manage multiple SSH connections
export class SSHConnectionPool {
  private connections: Map<string, SSHConnection> = new Map();

  async getConnection(connectionId: string, config: SSHConnectionConfig): Promise<SSHConnection> {
    let connection = this.connections.get(connectionId);
    
    if (!connection) {
      connection = new SSHConnection(config);
      this.connections.set(connectionId, connection);
      await connection.connect();
    } else if (!connection.isActive()) {
      await connection.connect();
    }

    return connection;
  }

  async closeConnection(connectionId: string): Promise<void> {
    const connection = this.connections.get(connectionId);
    if (connection) {
      connection.disconnect();
      this.connections.delete(connectionId);
    }
  }

  closeAll(): void {
    for (const connection of this.connections.values()) {
      connection.disconnect();
    }
    this.connections.clear();
  }
}
```

--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------

```typescript
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import type { ShellConfig } from '../types/config.js';
const execAsync = promisify(exec);

export function extractCommandName(command: string): string {
    // Remove any path components
    const basename = path.basename(command);
    // Remove extension
    return basename.replace(/\.(exe|cmd|bat)$/i, '').toLowerCase();
}

export function isCommandBlocked(command: string, blockedCommands: string[]): boolean {
    const commandName = extractCommandName(command.toLowerCase());
    return blockedCommands.some(blocked => 
        commandName === blocked.toLowerCase() ||
        commandName === `${blocked.toLowerCase()}.exe` ||
        commandName === `${blocked.toLowerCase()}.cmd` ||
        commandName === `${blocked.toLowerCase()}.bat`
    );
}

export function isArgumentBlocked(args: string[], blockedArguments: string[]): boolean {
    return args.some(arg => 
        blockedArguments.some(blocked => 
            new RegExp(`^${blocked}$`, 'i').test(arg)
        )
    );
}

/**
 * Validates a command for a specific shell, checking for shell-specific blocked operators
 */
export function validateShellOperators(command: string, shellConfig: ShellConfig): void {
    // Skip validation if shell doesn't specify blocked operators
    if (!shellConfig.blockedOperators?.length) {
        return;
    }

    // Create regex pattern from blocked operators
    const operatorPattern = shellConfig.blockedOperators
        .map(op => op.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))  // Escape regex special chars
        .join('|');
    
    const regex = new RegExp(operatorPattern);
    if (regex.test(command)) {
        throw new Error(`Command contains blocked operators for this shell: ${shellConfig.blockedOperators.join(', ')}`);
    }
}

/**
 * Parse a command string into command and arguments, properly handling paths with spaces and quotes
 */
export function parseCommand(fullCommand: string): { command: string; args: string[] } {
    fullCommand = fullCommand.trim();
    if (!fullCommand) {
        return { command: '', args: [] };
    }

    const tokens: string[] = [];
    let current = '';
    let inQuotes = false;
    let quoteChar = '';

    // Parse into tokens, preserving quoted strings
    for (let i = 0; i < fullCommand.length; i++) {
        const char = fullCommand[i];

        // Handle quotes
        if ((char === '"' || char === "'") && (!inQuotes || char === quoteChar)) {
            if (inQuotes) {
                tokens.push(current);
                current = '';
            }
            inQuotes = !inQuotes;
            quoteChar = inQuotes ? char : '';
            continue;
        }

        // Handle spaces outside quotes
        if (char === ' ' && !inQuotes) {
            if (current) {
                tokens.push(current);
                current = '';
            }
            continue;
        }

        current += char;
    }

    // Add any remaining token
    if (current) {
        tokens.push(current);
    }

    // Handle empty input
    if (tokens.length === 0) {
        return { command: '', args: [] };
    }

    // First, check if this is a single-token command
    if (!tokens[0].includes(' ') && !tokens[0].includes('\\')) {
        return {
            command: tokens[0],
            args: tokens.slice(1)
        };
    }

    // Special handling for Windows paths with spaces
    let commandTokens: string[] = [];
    let i = 0;

    // Keep processing tokens until we find a complete command path
    while (i < tokens.length) {
        commandTokens.push(tokens[i]);
        const potentialCommand = commandTokens.join(' ');

        // Check if this could be a complete command path
        if (/\.(exe|cmd|bat)$/i.test(potentialCommand) || 
            (!potentialCommand.includes('\\') && commandTokens.length === 1)) {
            return {
                command: potentialCommand,
                args: tokens.slice(i + 1)
            };
        }

        // If this is part of a path, keep looking
        if (potentialCommand.includes('\\')) {
            i++;
            continue;
        }

        // If we get here, treat the first token as the command
        return {
            command: tokens[0],
            args: tokens.slice(1)
        };
    }

    // If we get here, use all collected tokens as the command
    return {
        command: commandTokens.join(' '),
        args: tokens.slice(commandTokens.length)
    };
}

export function isPathAllowed(testPath: string, allowedPaths: string[]): boolean {
    const normalizedPath = path.normalize(testPath).toLowerCase();
    return allowedPaths.some(allowedPath => {
        const normalizedAllowedPath = path.normalize(allowedPath).toLowerCase();
        return normalizedPath.startsWith(normalizedAllowedPath);
    });
}

export function validateWorkingDirectory(dir: string, allowedPaths: string[]): void {
    if (!path.isAbsolute(dir)) {
        throw new Error('Working directory must be an absolute path');
    }

    if (!isPathAllowed(dir, allowedPaths)) {
        const allowedPathsStr = allowedPaths.join(', ');
        throw new Error(
            `Working directory must be within allowed paths: ${allowedPathsStr}`
        );
    }
}

export function normalizeWindowsPath(inputPath: string): string {
    // Convert forward slashes to backslashes
    let normalized = inputPath.replace(/\//g, '\\');
    
    // Handle Windows drive letter
    if (/^[a-zA-Z]:\\.+/.test(normalized)) {
        // Already in correct form
        return path.normalize(normalized);
    }
    
    // Handle paths without drive letter
    if (normalized.startsWith('\\')) {
        // Assume C: drive if not specified
        normalized = `C:${normalized}`;
    }
    
    return path.normalize(normalized);
}
```

--------------------------------------------------------------------------------
/src/utils/config.ts:
--------------------------------------------------------------------------------

```typescript
import fs from 'fs';
import path from 'path';
import os from 'os';
import { ServerConfig, ShellConfig } from '../types/config.js';

const defaultValidatePathRegex = /^[a-zA-Z]:\\(?:[^<>:"/\\|?*]+\\)*[^<>:"/\\|?*]*$/;

export const DEFAULT_CONFIG: ServerConfig = {
  security: {
    maxCommandLength: 2000,
    blockedCommands: [
      'rm', 'del', 'rmdir', 'format',
      'shutdown', 'restart',
      'reg', 'regedit',
      'net', 'netsh',
      'takeown', 'icacls'
    ],
    blockedArguments: [
      "--exec", "-e", "/c", "-enc", "-encodedcommand",
      "-command", "--interactive", "-i", "--login", "--system"
    ],
    allowedPaths: [
      os.homedir(),
      process.cwd()
    ],
    restrictWorkingDirectory: true,
    logCommands: true,
    maxHistorySize: 1000,
    commandTimeout: 30,
    enableInjectionProtection: true
  },
  shells: {
    powershell: {
      enabled: true,
      command: 'powershell.exe',
      args: ['-NoProfile', '-NonInteractive', '-Command'],
      validatePath: (dir: string) => dir.match(defaultValidatePathRegex) !== null,
      blockedOperators: ['&', '|', ';', '`']
    },
    cmd: {
      enabled: true,
      command: 'cmd.exe',
      args: ['/c'],
      validatePath: (dir: string) => dir.match(defaultValidatePathRegex) !== null,
      blockedOperators: ['&', '|', ';', '`']
    },
    gitbash: {
      enabled: true,
      command: 'C:\\Program Files\\Git\\bin\\bash.exe',
      args: ['-c'],
      validatePath: (dir: string) => dir.match(defaultValidatePathRegex) !== null,
      blockedOperators: ['&', '|', ';', '`']
    }
  },
  ssh: {
    enabled: false,
    defaultTimeout: 30,
    maxConcurrentSessions: 5,
    keepaliveInterval: 10000,
    keepaliveCountMax: 3,
    readyTimeout: 20000,
    connections: {}
  }
};

export function loadConfig(configPath?: string): ServerConfig {
  // If no config path provided, look in default locations
  const configLocations = [
    configPath,
    path.join(process.cwd(), 'config.json'),
    path.join(os.homedir(), '.win-cli-mcp', 'config.json')
  ].filter(Boolean);

  let loadedConfig: Partial<ServerConfig> = {};

  for (const location of configLocations) {
    if (!location) continue;
    
    try {
      if (fs.existsSync(location)) {
        const fileContent = fs.readFileSync(location, 'utf8');
        loadedConfig = JSON.parse(fileContent);
        console.error(`Loaded config from ${location}`);
        break;
      }
    } catch (error) {
      console.error(`Error loading config from ${location}:`, error);
    }
  }

  // Use defaults only if no config was loaded
  const mergedConfig = Object.keys(loadedConfig).length > 0 
    ? mergeConfigs(DEFAULT_CONFIG, loadedConfig)
    : DEFAULT_CONFIG;

  // Validate the merged config
  validateConfig(mergedConfig);

  return mergedConfig;
}

function mergeConfigs(defaultConfig: ServerConfig, userConfig: Partial<ServerConfig>): ServerConfig {
  const merged: ServerConfig = {
    security: {
      // If user provided security config, use it entirely, otherwise use default
      ...(userConfig.security || defaultConfig.security)
    },
    shells: {
      // Same for each shell - if user provided config, use it entirely
      powershell: userConfig.shells?.powershell || defaultConfig.shells.powershell,
      cmd: userConfig.shells?.cmd || defaultConfig.shells.cmd,
      gitbash: userConfig.shells?.gitbash || defaultConfig.shells.gitbash
    },
    ssh: {
      // Merge SSH config
      ...(defaultConfig.ssh),
      ...(userConfig.ssh || {}),
      // Ensure connections are merged
      connections: {
        ...(defaultConfig.ssh.connections),
        ...(userConfig.ssh?.connections || {})
      }
    }
  };

  // Only add validatePath functions and blocked operators if they don't exist
  for (const [key, shell] of Object.entries(merged.shells) as [keyof typeof merged.shells, ShellConfig][]) {
    if (!shell.validatePath) {
      shell.validatePath = defaultConfig.shells[key].validatePath;
    }
    if (!shell.blockedOperators) {
      shell.blockedOperators = defaultConfig.shells[key].blockedOperators;
    }
  }

  return merged;
}

function validateConfig(config: ServerConfig): void {
  // Validate security settings
  if (config.security.maxCommandLength < 1) {
    throw new Error('maxCommandLength must be positive');
  }

  if (config.security.maxHistorySize < 1) {
    throw new Error('maxHistorySize must be positive');
  }

  // Validate shell configurations
  for (const [shellName, shell] of Object.entries(config.shells)) {
    if (shell.enabled && (!shell.command || !shell.args)) {
      throw new Error(`Invalid configuration for ${shellName}: missing command or args`);
    }
  }

  // Validate timeout (minimum 1 second)
  if (config.security.commandTimeout < 1) {
    throw new Error('commandTimeout must be at least 1 second');
  }

  // Validate SSH configuration
  if (config.ssh.enabled) {
    if (config.ssh.defaultTimeout < 1) {
      throw new Error('SSH defaultTimeout must be at least 1 second');
    }
    if (config.ssh.maxConcurrentSessions < 1) {
      throw new Error('SSH maxConcurrentSessions must be at least 1');
    }
    if (config.ssh.keepaliveInterval < 1000) {
      throw new Error('SSH keepaliveInterval must be at least 1000ms');
    }
    if (config.ssh.readyTimeout < 1000) {
      throw new Error('SSH readyTimeout must be at least 1000ms');
    }

    // Validate individual connections
    for (const [connId, conn] of Object.entries(config.ssh.connections)) {
      if (!conn.host || !conn.username || (!conn.password && !conn.privateKeyPath)) {
        throw new Error(`Invalid SSH connection config for '${connId}': missing required fields`);
      }
      if (conn.port && (conn.port < 1 || conn.port > 65535)) {
        throw new Error(`Invalid SSH port for '${connId}': must be between 1 and 65535`);
      }
    }
  }
}

// Helper function to create a default config file
export function createDefaultConfig(configPath: string): void {
  const dirPath = path.dirname(configPath);
  
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true });
  }

  // Create a JSON-safe version of the config (excluding functions)
  const configForSave = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
  fs.writeFileSync(configPath, JSON.stringify(configForSave, null, 2));
}
```

--------------------------------------------------------------------------------
/tests/validation.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, expect, test, jest } from '@jest/globals';
import path from 'path';
import {
  extractCommandName,
  isCommandBlocked,
  isArgumentBlocked,
  parseCommand,
  isPathAllowed,
  validateWorkingDirectory,
  normalizeWindowsPath,
  validateShellOperators
} from '../src/utils/validation.js';
import type { ShellConfig } from '../src/types/config.js';

// Mock child_process exec
jest.mock('child_process', () => ({
  exec: jest.fn()
}));


describe('Command Name Extraction', () => {
  test('extractCommandName handles various formats', () => {
    expect(extractCommandName('cmd.exe')).toBe('cmd');
    expect(extractCommandName('C:\\Windows\\System32\\cmd.exe')).toBe('cmd');
    expect(extractCommandName('powershell.exe')).toBe('powershell');
    expect(extractCommandName('git.cmd')).toBe('git');
    expect(extractCommandName('program')).toBe('program');
    expect(extractCommandName('path/to/script.bat')).toBe('script');
  });

  test('extractCommandName is case insensitive', () => {
    expect(extractCommandName('CMD.EXE')).toBe('cmd');
    expect(extractCommandName('PowerShell.Exe')).toBe('powershell');
  });
});

describe('Command Blocking', () => {
  const blockedCommands = ['rm', 'del', 'format'];

  test('isCommandBlocked identifies blocked commands', () => {
    expect(isCommandBlocked('rm', blockedCommands)).toBe(true);
    expect(isCommandBlocked('rm.exe', blockedCommands)).toBe(true);
    expect(isCommandBlocked('C:\\Windows\\System32\\rm.exe', blockedCommands)).toBe(true);
    expect(isCommandBlocked('notepad.exe', blockedCommands)).toBe(false);
  });

  test('isCommandBlocked is case insensitive', () => {
    expect(isCommandBlocked('RM.exe', blockedCommands)).toBe(true);
    expect(isCommandBlocked('DeL.exe', blockedCommands)).toBe(true);
    expect(isCommandBlocked('FORMAT.EXE', blockedCommands)).toBe(true);
  });

  test('isCommandBlocked handles different extensions', () => {
    expect(isCommandBlocked('rm.cmd', blockedCommands)).toBe(true);
    expect(isCommandBlocked('del.bat', blockedCommands)).toBe(true);
    expect(isCommandBlocked('format.com', blockedCommands)).toBe(false); // Should only match .exe, .cmd, .bat
  });
});

describe('Argument Blocking', () => {
  const blockedArgs = ['--system', '-rf', '--exec'];

  test('isArgumentBlocked identifies blocked arguments', () => {
    expect(isArgumentBlocked(['--help', '--system'], blockedArgs)).toBe(true);
    expect(isArgumentBlocked(['-rf'], blockedArgs)).toBe(true);
    expect(isArgumentBlocked(['--safe', '--normal'], blockedArgs)).toBe(false);
  });

  test('isArgumentBlocked is case insensitive for security', () => {
    expect(isArgumentBlocked(['--SYSTEM'], blockedArgs)).toBe(true);
    expect(isArgumentBlocked(['-RF'], blockedArgs)).toBe(true);
    expect(isArgumentBlocked(['--SyStEm'], blockedArgs)).toBe(true);
  });

  test('isArgumentBlocked handles multiple arguments', () => {
    expect(isArgumentBlocked(['--safe', '--exec', '--other'], blockedArgs)).toBe(true);
    expect(isArgumentBlocked(['arg1', 'arg2', '--help'], blockedArgs)).toBe(false);
  });
});

describe('Command Parsing', () => {
  test('parseCommand handles basic commands', () => {
    expect(parseCommand('dir')).toEqual({ command: 'dir', args: [] });
    expect(parseCommand('echo hello')).toEqual({ command: 'echo', args: ['hello'] });
  });

  test('parseCommand handles quoted arguments', () => {
    expect(parseCommand('echo "hello world"')).toEqual({ 
      command: 'echo', 
      args: ['hello world']
    });
    expect(parseCommand('echo "first" "second"')).toEqual({
      command: 'echo',
      args: ['first', 'second']
    });
  });

  test('parseCommand handles paths with spaces', () => {
    expect(parseCommand('C:\\Program Files\\Git\\bin\\git.exe status')).toEqual({
      command: 'C:\\Program Files\\Git\\bin\\git.exe',
      args: ['status']
    });
  });

  test('parseCommand handles empty input', () => {
    expect(parseCommand('')).toEqual({ command: '', args: [] });
    expect(parseCommand('  ')).toEqual({ command: '', args: [] });
  });

  test('parseCommand handles mixed quotes', () => {
    expect(parseCommand('git commit -m "first commit" --author="John Doe"')).toEqual({
      command: 'git',
      args: ['commit', '-m', 'first commit', '--author=John Doe']
    });
  });
});

describe('Path Validation', () => {
  const allowedPaths = [
    'C:\\Users\\test',
    'D:\\Projects'
  ];

  test('isPathAllowed validates paths correctly', () => {
    expect(isPathAllowed('C:\\Users\\test\\docs', allowedPaths)).toBe(true);
    expect(isPathAllowed('C:\\Users\\test', allowedPaths)).toBe(true);
    expect(isPathAllowed('D:\\Projects\\code', allowedPaths)).toBe(true);
    expect(isPathAllowed('E:\\NotAllowed', allowedPaths)).toBe(false);
  });

  test('isPathAllowed is case insensitive', () => {
    expect(isPathAllowed('c:\\users\\TEST\\docs', allowedPaths)).toBe(true);
    expect(isPathAllowed('D:\\PROJECTS\\code', allowedPaths)).toBe(true);
  });

  test('validateWorkingDirectory throws for invalid paths', () => {
    expect(() => validateWorkingDirectory('relative/path', allowedPaths))
      .toThrow('Working directory must be an absolute path');
    expect(() => validateWorkingDirectory('E:\\NotAllowed', allowedPaths))
      .toThrow('Working directory must be within allowed paths');
  });
});

describe('Path Normalization', () => {
  test('normalizeWindowsPath handles various formats', () => {
    expect(normalizeWindowsPath('C:/Users/test')).toBe('C:\\Users\\test');
    expect(normalizeWindowsPath('\\Users\\test')).toBe('C:\\Users\\test');
    expect(normalizeWindowsPath('D:\\Projects')).toBe('D:\\Projects');
  });

  test('normalizeWindowsPath removes redundant separators', () => {
    expect(normalizeWindowsPath('C:\\\\Users\\\\test')).toBe('C:\\Users\\test');
    expect(normalizeWindowsPath('C:/Users//test')).toBe('C:\\Users\\test');
  });
});

describe('Shell Operator Validation', () => {
  const powershellConfig: ShellConfig = {
    enabled: true,
    command: 'powershell.exe',
    args: ['-Command'],
    blockedOperators: ['&', ';', '`']
  };

  test('validateShellOperators blocks dangerous operators', () => {
    expect(() => validateShellOperators('Get-Process & Get-Service', powershellConfig))
      .toThrow();
    expect(() => validateShellOperators('Get-Process; Start-Sleep', powershellConfig))
      .toThrow();
  });

  test('validateShellOperators allows safe operators when configured', () => {
    expect(() => validateShellOperators('Get-Process | Select-Object Name', powershellConfig))
      .not.toThrow();
    expect(() => validateShellOperators('$var = Get-Process', powershellConfig))
      .not.toThrow();
  });

  test('validateShellOperators respects shell config', () => {
    const customConfig: ShellConfig = {
      enabled: true,
      command: 'custom.exe',
      args: [],
      blockedOperators: ['|'] // Block only pipe operator
    };

    expect(() => validateShellOperators('cmd & echo test', customConfig))
      .not.toThrow();
    expect(() => validateShellOperators('cmd | echo test', customConfig))
      .toThrow();
  });
});
```

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

```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ErrorCode,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { 
  isCommandBlocked,
  isArgumentBlocked,
  parseCommand,
  extractCommandName,
  validateShellOperators
} from './utils/validation.js';
import { spawn } from 'child_process';
import { z } from 'zod';
import path from 'path';
import { loadConfig, createDefaultConfig } from './utils/config.js';
import type { ServerConfig, CommandHistoryEntry, SSHConnectionConfig } from './types/config.js';
import { SSHConnectionPool } from './utils/ssh.js';
import { createRequire } from 'module';
import { createSSHConnection, readSSHConnections, updateSSHConnection, deleteSSHConnection } from './utils/sshManager.js';
const require = createRequire(import.meta.url);
const packageJson = require('../package.json');

// Parse command line arguments using yargs
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';

const parseArgs = async () => {
  return yargs(hideBin(process.argv))
    .option('config', {
      alias: 'c',
      type: 'string',
      description: 'Path to config file'
    })
    .option('init-config', {
      type: 'string',
      description: 'Create a default config file at the specified path'
    })
    .help()
    .parse();
};

class CLIServer {
  private server: Server;
  private allowedPaths: Set<string>;
  private blockedCommands: Set<string>;
  private commandHistory: CommandHistoryEntry[];
  private config: ServerConfig;
  private sshPool: SSHConnectionPool;

  constructor(config: ServerConfig) {
    this.config = config;
    this.server = new Server({
      name: "windows-cli-server",
      version: packageJson.version,
    }, {
      capabilities: {
        tools: {},
        resources: {}  // Add resources capability
      }
    });

    // Initialize from config
    this.allowedPaths = new Set(config.security.allowedPaths);
    this.blockedCommands = new Set(config.security.blockedCommands);
    this.commandHistory = [];
    this.sshPool = new SSHConnectionPool();

    this.setupHandlers();
  }

  private validateCommand(shell: keyof ServerConfig['shells'], command: string): void {
    // Check for command chaining/injection attempts if enabled
    if (this.config.security.enableInjectionProtection) {
      // Get shell-specific config
      const shellConfig = this.config.shells[shell];
      
      // Use shell-specific operator validation
      validateShellOperators(command, shellConfig);
    }
  
    const { command: executable, args } = parseCommand(command);
  
    // Check for blocked commands
    if (isCommandBlocked(executable, Array.from(this.blockedCommands))) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        `Command is blocked: "${extractCommandName(executable)}"`
      );
    }
  
    // Check for blocked arguments
    if (isArgumentBlocked(args, this.config.security.blockedArguments)) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        'One or more arguments are blocked. Check configuration for blocked patterns.'
      );
    }
  
    // Validate command length
    if (command.length > this.config.security.maxCommandLength) {
      throw new McpError(
        ErrorCode.InvalidRequest,
        `Command exceeds maximum length of ${this.config.security.maxCommandLength}`
      );
    }
  }

  /**
   * Escapes special characters in a string for use in a regular expression
   * @param text The string to escape
   * @returns The escaped string
   */
  private escapeRegex(text: string): string {
    return text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  }

  private setupHandlers(): void {
    // List available resources
    this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
      const sshConnections = readSSHConnections() as Record<string, any>;
      
      // Create resources for each SSH connection
      const resources = Object.entries(sshConnections).map(([id, config]) => ({
        uri: `ssh://${id}`,
        name: `SSH Connection: ${id}`,
        description: `SSH connection to ${config.host}:${config.port} as ${config.username}`,
        mimeType: "application/json"
      }));
      
      // Add a resource for the current working directory
      resources.push({
        uri: "cli://currentdir",
        name: "Current Working Directory",
        description: "The current working directory of the CLI server",
        mimeType: "text/plain"
      });
      
      // Add a resource for SSH configuration
      resources.push({
        uri: "ssh://config",
        name: "SSH Configuration",
        description: "All SSH connection configurations",
        mimeType: "application/json"
      });

      // Add a resource for CLI configuration
      resources.push({
        uri: "cli://config",
        name: "CLI Server Configuration",
        description: "Main CLI server configuration (excluding sensitive data)",
        mimeType: "application/json"
      });
      
      return { resources };
    });

    // Read resource content
    this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      const uri = request.params.uri;
      
      // Handle SSH connection resources
      if (uri.startsWith("ssh://") && uri !== "ssh://config") {
        const connectionId = uri.slice(6); // Remove "ssh://" prefix
        const connections = readSSHConnections() as Record<string, any>;
        const connectionConfig = connections[connectionId];
        
        if (!connectionConfig) {
          throw new McpError(
            ErrorCode.InvalidRequest,
            `Unknown SSH connection: ${connectionId}`
          );
        }
        
        // Return connection details (excluding sensitive info)
        const safeConfig = { ...connectionConfig };
        
        // Remove sensitive information
        if (safeConfig.password) {
          safeConfig.password = "********";
        }
        
        return {
          contents: [{
            uri,
            mimeType: "application/json",
            text: JSON.stringify(safeConfig, null, 2)
          }]
        };
      }
      
      // Handle SSH configuration resource
      if (uri === "ssh://config") {
        const connections = readSSHConnections() as Record<string, any>;
        const safeConnections = { ...connections };
        
        // Remove sensitive information from all connections
        for (const connection of Object.values(safeConnections)) {
          if (connection.password) {
            connection.password = "********";
          }
        }
        
        return {
          contents: [{
            uri,
            mimeType: "application/json",
            text: JSON.stringify({
              enabled: this.config.ssh.enabled,
              defaultTimeout: this.config.ssh.defaultTimeout,
              maxConcurrentSessions: this.config.ssh.maxConcurrentSessions,
              connections: safeConnections
            }, null, 2)
          }]
        };
      }
      
      // Handle current directory resource
      if (uri === "cli://currentdir") {
        const currentDir = process.cwd();
        return {
          contents: [{
            uri,
            mimeType: "text/plain",
            text: currentDir
          }]
        };
      }
      
      // Handle CLI configuration resource
      if (uri === "cli://config") {
        // Create a safe copy of config (excluding sensitive information)
        const safeConfig = {
          security: {
            ...this.config.security,
          },
          shells: {
            ...this.config.shells
          },
          ssh: {
            enabled: this.config.ssh.enabled,
            defaultTimeout: this.config.ssh.defaultTimeout,
            maxConcurrentSessions: this.config.ssh.maxConcurrentSessions,
            connections: Object.keys(this.config.ssh.connections).length
          }
        };
        
        return {
          contents: [{
            uri,
            mimeType: "application/json",
            text: JSON.stringify(safeConfig, null, 2)
          }]
        };
      }
      
      throw new McpError(
        ErrorCode.InvalidRequest,
        `Unknown resource URI: ${uri}`
      );
    });

    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: "execute_command",
          description: `Execute a command in the specified shell (powershell, cmd, or gitbash)

Example usage (PowerShell):
\`\`\`json
{
  "shell": "powershell",
  "command": "Get-Process | Select-Object -First 5",
  "workingDir": "C:\\Users\\username"
}
\`\`\`

Example usage (CMD):
\`\`\`json
{
  "shell": "cmd",
  "command": "dir /b",
  "workingDir": "C:\\Projects"
}
\`\`\`

Example usage (Git Bash):
\`\`\`json
{
  "shell": "gitbash",
  "command": "ls -la",
  "workingDir": "/c/Users/username"
}
\`\`\``,
          inputSchema: {
            type: "object",
            properties: {
              shell: {
                type: "string",
                enum: Object.keys(this.config.shells).filter(shell => 
                  this.config.shells[shell as keyof typeof this.config.shells].enabled
                ),
                description: "Shell to use for command execution"
              },
              command: {
                type: "string",
                description: "Command to execute"
              },
              workingDir: {
                type: "string",
                description: "Working directory for command execution (optional)"
              }
            },
            required: ["shell", "command"]
          }
        },
        {
          name: "get_command_history",
          description: `Get the history of executed commands

Example usage:
\`\`\`json
{
  "limit": 5
}
\`\`\`

Example response:
\`\`\`json
[
  {
    "command": "Get-Process",
    "output": "...",
    "timestamp": "2024-03-20T10:30:00Z",
    "exitCode": 0
  }
]
\`\`\``,
          inputSchema: {
            type: "object",
            properties: {
              limit: {
                type: "number",
                description: `Maximum number of history entries to return (default: 10, max: ${this.config.security.maxHistorySize})`
              }
            }
          }
        },
        {
          name: "ssh_execute",
          description: `Execute a command on a remote host via SSH

Example usage:
\`\`\`json
{
  "connectionId": "raspberry-pi",
  "command": "uname -a"
}
\`\`\`

Configuration required in config.json:
\`\`\`json
{
  "ssh": {
    "enabled": true,
    "connections": {
      "raspberry-pi": {
        "host": "raspberrypi.local",
        "port": 22,
        "username": "pi",
        "password": "raspberry"
      }
    }
  }
}
\`\`\``,
          inputSchema: {
            type: "object",
            properties: {
              connectionId: {
                type: "string",
                description: "ID of the SSH connection to use",
                enum: Object.keys(this.config.ssh.connections)
              },
              command: {
                type: "string",
                description: "Command to execute"
              }
            },
            required: ["connectionId", "command"]
          }
        },
        {
          name: "ssh_disconnect",
          description: `Disconnect from an SSH server

Example usage:
\`\`\`json
{
  "connectionId": "raspberry-pi"
}
\`\`\`

Use this to cleanly close SSH connections when they're no longer needed.`,
          inputSchema: {
            type: "object",
            properties: {
              connectionId: {
                type: "string",
                description: "ID of the SSH connection to disconnect",
                enum: Object.keys(this.config.ssh.connections)
              }
            },
            required: ["connectionId"]
          }
        },
        {
          name: "create_ssh_connection",
          description: "Create a new SSH connection",
          inputSchema: {
            type: "object",
            properties: {
              connectionId: {
                type: "string",
                description: "ID of the SSH connection"
              },
              connectionConfig: {
                type: "object",
                properties: {
                  host: {
                    type: "string",
                    description: "Host of the SSH connection"
                  },
                  port: {
                    type: "number",
                    description: "Port of the SSH connection"
                  },
                  username: {
                    type: "string",
                    description: "Username for the SSH connection"
                  },
                  password: {
                    type: "string",
                    description: "Password for the SSH connection"
                  },
                  privateKeyPath: {
                    type: "string",
                    description: "Path to the private key for the SSH connection"
                  }
                },
                required: ["connectionId", "connectionConfig"]
              }
            }
          }
        },
        {
          name: "read_ssh_connections",
          description: "Read all SSH connections",
          inputSchema: {
            type: "object",
            properties: {} // No input parameters needed
          }
        },
        {
          name: "update_ssh_connection",
          description: "Update an existing SSH connection",
          inputSchema: {
            type: "object",
            properties: {
              connectionId: {
                type: "string",
                description: "ID of the SSH connection to update"
              },
              connectionConfig: {
                type: "object",
                properties: {
                  host: {
                    type: "string",
                    description: "Host of the SSH connection"
                  },
                  port: {
                    type: "number",
                    description: "Port of the SSH connection"
                  },
                  username: {
                    type: "string",
                    description: "Username for the SSH connection"
                  },
                  password: {
                    type: "string",
                    description: "Password for the SSH connection"
                  },
                  privateKeyPath: {
                    type: "string",
                    description: "Path to the private key for the SSH connection"
                  }
                },
                required: ["connectionId", "connectionConfig"]
              }
            }
          }
        },
        {
          name: "delete_ssh_connection",
          description: "Delete an existing SSH connection",
          inputSchema: {
            type: "object",
            properties: {
              connectionId: {
                type: "string",
                description: "ID of the SSH connection to delete"
              }
            },
            required: ["connectionId"]
          }
        },
        {
          name: "get_current_directory",
          description: "Get the current working directory",
          inputSchema: {
            type: "object",
            properties: {} // No input parameters needed
          }
        }
      ]
    }));

    // Handle tool execution
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      try {
        switch (request.params.name) {
          case "execute_command": {
            const args = z.object({
              shell: z.enum(Object.keys(this.config.shells).filter(shell => 
                this.config.shells[shell as keyof typeof this.config.shells].enabled
              ) as [string, ...string[]]),
              command: z.string(),
              workingDir: z.string().optional()
            }).parse(request.params.arguments);

            // Validate command
            this.validateCommand(args.shell as keyof ServerConfig['shells'], args.command);

            // Validate working directory if provided
            let workingDir = args.workingDir ? 
              path.resolve(args.workingDir) : 
              process.cwd();

            const shellKey = args.shell as keyof typeof this.config.shells;
            const shellConfig = this.config.shells[shellKey];
            
            if (this.config.security.restrictWorkingDirectory) {
              const isAllowedPath = Array.from(this.allowedPaths).some(
                allowedPath => workingDir.startsWith(allowedPath)
              );

              if (!isAllowedPath) {
                throw new McpError(
                  ErrorCode.InvalidRequest,
                  `Working directory (${workingDir}) outside allowed paths. Consult the server admin for configuration changes (config.json - restrictWorkingDirectory, allowedPaths).`
                );
              }
            }

            // Execute command
            return new Promise((resolve, reject) => {
              let shellProcess: ReturnType<typeof spawn>;
              
              try {
                shellProcess = spawn(
                  shellConfig.command,
                  [...shellConfig.args, args.command],
                  { cwd: workingDir, stdio: ['pipe', 'pipe', 'pipe'] }
                );
              } catch (err) {
                throw new McpError(
                  ErrorCode.InternalError,
                  `Failed to start shell process: ${err instanceof Error ? err.message : String(err)}. Consult the server admin for configuration changes (config.json - shells).`
                );
              }

              if (!shellProcess.stdout || !shellProcess.stderr) {
                throw new McpError(
                  ErrorCode.InternalError,
                  'Failed to initialize shell process streams'
                );
              }

              let output = '';
              let error = '';

              shellProcess.stdout.on('data', (data) => {
                output += data.toString();
              });

              shellProcess.stderr.on('data', (data) => {
                error += data.toString();
              });

              shellProcess.on('close', (code) => {
                // Prepare detailed result message
                let resultMessage = '';
                
                if (code === 0) {
                  resultMessage = output || 'Command completed successfully (no output)';
                } else {
                  resultMessage = `Command failed with exit code ${code}\n`;
                  if (error) {
                    resultMessage += `Error output:\n${error}\n`;
                  }
                  if (output) {
                    resultMessage += `Standard output:\n${output}`;
                  }
                  if (!error && !output) {
                    resultMessage += 'No error message or output was provided';
                  }
                }

                // Store in history if enabled
                if (this.config.security.logCommands) {
                  this.commandHistory.push({
                    command: args.command,
                    output: resultMessage,
                    timestamp: new Date().toISOString(),
                    exitCode: code ?? -1
                  });

                  // Trim history if needed
                  if (this.commandHistory.length > this.config.security.maxHistorySize) {
                    this.commandHistory = this.commandHistory.slice(-this.config.security.maxHistorySize);
                  }
                }

                resolve({
                  content: [{
                    type: "text",
                    text: resultMessage
                  }],
                  isError: code !== 0,
                  metadata: {
                    exitCode: code ?? -1,
                    shell: args.shell,
                    workingDirectory: workingDir
                  }
                });
              });

              // Handle process errors (e.g., shell crashes)
              shellProcess.on('error', (err) => {
                const errorMessage = `Shell process error: ${err.message}`;
                if (this.config.security.logCommands) {
                  this.commandHistory.push({
                    command: args.command,
                    output: errorMessage,
                    timestamp: new Date().toISOString(),
                    exitCode: -1
                  });
                }
                reject(new McpError(
                  ErrorCode.InternalError,
                  errorMessage
                ));
              });

              // Set configurable timeout to prevent hanging
              const timeout = setTimeout(() => {
                shellProcess.kill();
                const timeoutMessage = `Command execution timed out after ${this.config.security.commandTimeout} seconds. Consult the server admin for configuration changes (config.json - commandTimeout).`;
                if (this.config.security.logCommands) {
                  this.commandHistory.push({
                    command: args.command,
                    output: timeoutMessage,
                    timestamp: new Date().toISOString(),
                    exitCode: -1
                  });
                }
                reject(new McpError(
                  ErrorCode.InternalError,
                  timeoutMessage
                ));
              }, this.config.security.commandTimeout * 1000);

              shellProcess.on('close', () => clearTimeout(timeout));
            });
          }

          case "get_command_history": {
            if (!this.config.security.logCommands) {
              return {
                content: [{
                  type: "text",
                  text: "Command history is disabled in configuration. Consult the server admin for configuration changes (config.json - logCommands)."
                }]
              };
            }

            const args = z.object({
              limit: z.number()
                .min(1)
                .max(this.config.security.maxHistorySize)
                .optional()
                .default(10)
            }).parse(request.params.arguments);

            const history = this.commandHistory
              .slice(-args.limit)
              .map(entry => ({
                ...entry,
                output: entry.output.slice(0, 1000) // Limit output size
              }));

            return {
              content: [{
                type: "text",
                text: JSON.stringify(history, null, 2)
              }]
            };
          }

          case "ssh_execute": {
            if (!this.config.ssh.enabled) {
              throw new McpError(
                ErrorCode.InvalidRequest,
                "SSH support is disabled in configuration"
              );
            }

            const args = z.object({
              connectionId: z.string(),
              command: z.string()
            }).parse(request.params.arguments);

            const connectionConfig = this.config.ssh.connections[args.connectionId];
            if (!connectionConfig) {
              throw new McpError(
                ErrorCode.InvalidRequest,
                `Unknown SSH connection ID: ${args.connectionId}`
              );
            }

            try {
              // Validate command
              this.validateCommand('cmd', args.command);

              const connection = await this.sshPool.getConnection(args.connectionId, connectionConfig);
              const { output, exitCode } = await connection.executeCommand(args.command);

              // Store in history if enabled
              if (this.config.security.logCommands) {
                this.commandHistory.push({
                  command: args.command,
                  output,
                  timestamp: new Date().toISOString(),
                  exitCode,
                  connectionId: args.connectionId
                });

                if (this.commandHistory.length > this.config.security.maxHistorySize) {
                  this.commandHistory = this.commandHistory.slice(-this.config.security.maxHistorySize);
                }
              }

              return {
                content: [{
                  type: "text",
                  text: output || 'Command completed successfully (no output)'
                }],
                isError: exitCode !== 0,
                metadata: {
                  exitCode,
                  connectionId: args.connectionId
                }
              };
            } catch (error) {
              const errorMessage = error instanceof Error ? error.message : String(error);
              if (this.config.security.logCommands) {
                this.commandHistory.push({
                  command: args.command,
                  output: `SSH error: ${errorMessage}`,
                  timestamp: new Date().toISOString(),
                  exitCode: -1,
                  connectionId: args.connectionId
                });
              }
              throw new McpError(
                ErrorCode.InternalError,
                `SSH error: ${errorMessage}`
              );
            }
          }

          case "ssh_disconnect": {
            if (!this.config.ssh.enabled) {
              throw new McpError(
                ErrorCode.InvalidRequest,
                "SSH support is disabled in configuration"
              );
            }

            const args = z.object({
              connectionId: z.string()
            }).parse(request.params.arguments);

            await this.sshPool.closeConnection(args.connectionId);
            return {
              content: [{
                type: "text",
                text: `Disconnected from ${args.connectionId}`
              }]
            };
          }

          case 'create_ssh_connection': {
            const args = z.object({
              connectionId: z.string(),
              connectionConfig: z.object({
                host: z.string(),
                port: z.number(),
                username: z.string(),
                password: z.string().optional(),
                privateKeyPath: z.string().optional(),
              })
            }).parse(request.params.arguments);
            createSSHConnection(args.connectionId, args.connectionConfig);
            return { content: [{ type: 'text', text: 'SSH connection created successfully.' }] };
          }

          case 'read_ssh_connections': {
            const connections = readSSHConnections();
            return { content: [{ type: 'json', text: JSON.stringify(connections, null, 2) }] };
          }

          case 'update_ssh_connection': {
            const args = z.object({
              connectionId: z.string(),
              connectionConfig: z.object({
                host: z.string(),
                port: z.number(),
                username: z.string(),
                password: z.string().optional(),
                privateKeyPath: z.string().optional(),
              })
            }).parse(request.params.arguments);
            updateSSHConnection(args.connectionId, args.connectionConfig);
            return { content: [{ type: 'text', text: 'SSH connection updated successfully.' }] };
          }

          case 'delete_ssh_connection': {
            const args = z.object({
              connectionId: z.string(),
            }).parse(request.params.arguments);
            deleteSSHConnection(args.connectionId);
            return { content: [{ type: 'text', text: 'SSH connection deleted successfully.' }] };
          }

          case 'get_current_directory': {
            const currentDir = process.cwd();
            return { content: [{ type: 'text', text: `Current working directory: ${currentDir}` }] };
          }

          default:
            throw new McpError(
              ErrorCode.MethodNotFound,
              `Unknown tool: ${request.params.name}`
            );
        }
      } catch (error) {
        if (error instanceof z.ZodError) {
          throw new McpError(
            ErrorCode.InvalidParams,
            `Invalid arguments: ${error.errors.map(e => e.message).join(', ')}`
          );
        }
        throw error;
      }
    });
  }

  private async cleanup(): Promise<void> {
    this.sshPool.closeAll();
  }

  async run(): Promise<void> {
    const transport = new StdioServerTransport();
    
    // Set up cleanup handler
    process.on('SIGINT', async () => {
      await this.cleanup();
      process.exit(0);
    });
    
    await this.server.connect(transport);
    console.error("Windows CLI MCP Server running on stdio");
  }
}

// Start server
const main = async () => {
  try {
    const args = await parseArgs();
    
    // Handle --init-config flag
    if (args['init-config']) {
      try {
        createDefaultConfig(args['init-config'] as string);
        console.error(`Created default config at: ${args['init-config']}`);
        process.exit(0);
      } catch (error) {
        console.error('Failed to create config file:', error);
        process.exit(1);
      }
    }

    // Load configuration
    const config = loadConfig(args.config);
    
    const server = new CLIServer(config);
    await server.run();
  } catch (error) {
    console.error("Fatal error:", error);
    process.exit(1);
  }
};

main();
```