Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.15] - 2025-09-17

### Security
- Disabled shell parsing by default to prevent command injection in `execute_command`
- Added explicit environment-controlled opt-in for legacy shell mode

### Added
- New `CommandServiceOptions` API surface (`useShell`, `defaultTimeout`) and `isShellEnabled`
- Environment variable support for shell mode/timeout configuration
- Documentation updates covering safe defaults and opt-in shell behaviour

### Fixed
- Updated tests to cover literal argument handling and shell opt-in paths
- Manifest now exposes shell parsing toggle to MCP clients

## [2.0.14] - 2025-09-17

### Changed
- Internal release iteration superseded by 2.0.15

## [2.0.13] - 2025-03-14

### Fixed
Expand Down
91 changes: 65 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ An MCP (Model Context Protocol) server for executing shell commands across multi
- **Windows**: cmd.exe, PowerShell
- **macOS**: zsh, bash, sh
- **Linux**: bash, sh, zsh
- Shell parsing disabled by default to eliminate command-injection risk, with an explicit opt-in mode for trusted workflows
- Command whitelisting with security levels:
- **Safe**: Commands that can be executed without approval
- **Requires Approval**: Commands that need explicit approval before execution
Expand Down Expand Up @@ -153,32 +154,40 @@ If you prefer to use a local installation, add the following to your Roo Code MC
}
```

You can optionally specify a custom shell by adding a shell parameter:
You can optionally provide a trusted shell and opt into shell parsing by setting environment variables instead of command-line flags:

```json
"super-shell": {
"command": "node",
"args": [
"/path/to/super-shell-mcp/build/index.js",
"--shell=/usr/bin/bash"
"/path/to/super-shell-mcp/build/index.js"
],
"env": {
"CUSTOM_SHELL": "/usr/bin/bash",
"SUPER_SHELL_USE_SHELL": "true"
},
"alwaysAllow": [],
"disabled": false
}
```
Windows 11 example

Windows 11 example:
```json
"super-shell": {
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": [
"C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npx-cli.js",
"-y",
"super-shell-mcp",
"C:\\Users\\username"
],
"alwaysAllow": [],
"disabled": false
}
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": [
"C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npx-cli.js",
"-y",
"super-shell-mcp",
"C:\\Users\\username"
],
"env": {
"CUSTOM_SHELL": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"SUPER_SHELL_USE_SHELL": "true"
},
"alwaysAllow": [],
"disabled": false
}
```

#### Claude Desktop Configuration
Expand Down Expand Up @@ -230,19 +239,15 @@ For Windows users, the configuration file is typically located at `%APPDATA%\Cla
- zsh: `/usr/bin/zsh`


You can optionally specify a custom shell:
### Shell Execution Modes & Environment Variables

```json
"super-shell": {
"command": "node",
"args": [
"/path/to/super-shell-mcp/build/index.js",
"--shell=C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
],
"alwaysAllow": false,
"disabled": false
}
```
Shell parsing is **disabled by default** for security. Customise behaviour with the following environment variables:

- `SUPER_SHELL_USE_SHELL`: set to `true` (or `1/yes/on`) to enable shell parsing for trusted workflows. Omit or set to `false` to keep the safer default.
- `CUSTOM_SHELL`: optional path to the shell executable used when shell parsing is enabled.
- `SUPER_SHELL_COMMAND_TIMEOUT`: optional override (milliseconds) for the default 30s command timeout.

> ⚠️ Enabling shell parsing reintroduces the risk of command injection. Only enable it when you fully trust the command source and payload.

Replace `/path/to/super-shell-mcp` with the actual path where you cloned the repository.

Expand Down Expand Up @@ -580,6 +585,40 @@ The server includes a comprehensive logging system that writes logs to a file fo
- **Issue**: Need to add custom commands to whitelist
- **Solution**: Use the `add_to_whitelist` tool to add commands specific to your environment

## Known Issues

### Android Studio Otter 2 Feature Drop Compatibility

**Affected Version**: Android Studio Otter 2 Feature Drop | 2025.2.2 Canary 3 (Build #AI-252.25557.131.2522.14357309)

**Issue**: Android Studio's MCP client implementation incorrectly serializes array parameters as strings when calling the `execute_command` tool. This causes commands with arguments to fail with the error:

```
Error: Expected array, received string
```

**Example**:
```javascript
// This fails in Android Studio Otter 2
execute_command(
command = "git",
args = ["add", "."] // Sent as string '["add", "."]' instead of array
)
```

**Root Cause**: This is a bug in Android Studio's MCP client, not in super-shell-mcp. The server correctly defines `args` as type `array` in its schema, and the issue has been verified to work correctly with:
- Claude Desktop
- Official MCP SDK clients
- Other MCP-compatible tools

**Status**: This is an Android Studio bug. The super-shell-mcp server implements the MCP specification correctly.

**Workaround**: None currently available. Users experiencing this issue should report it to the Android Studio/JetBrains team.

**References**:
- Issue reported: [#20](https://github.com/cfdude/super-shell-mcp/issues/20)
- Related documentation: [Android Studio Gemini MCP Integration](https://developer.android.com/studio/gemini/add-mcp-server)

## License

This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
15 changes: 10 additions & 5 deletions jest.setup.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
const validateShellPath = (shellPath) => {
try {
return fs.existsSync(shellPath) && fs.statSync(shellPath).isFile();
} catch (error) {

Check warning on line 50 in jest.setup.cjs

View workflow job for this annotation

GitHub Actions / lint-and-build

'error' is defined but never used
return false;
}
};
Expand Down Expand Up @@ -101,19 +101,24 @@

// Mock the CommandService class
class CommandService extends EventEmitter {
constructor(shell, defaultTimeout = 30000) {
constructor(options = {}) {
super();
this.shell = shell || getDefaultShell();
this.shell = options.shell || getDefaultShell();
this.useShell = options.useShell ?? false;
this.whitelist = new Map();
this.pendingCommands = new Map();
this.defaultTimeout = defaultTimeout;
this.defaultTimeout = options.defaultTimeout ?? 30000;
this.initializeDefaultWhitelist();
}

getShell() {
return this.shell;
}

isShellEnabled() {
return this.useShell;
}

initializeDefaultWhitelist() {
const platform = detectPlatform();
const commands = [];
Expand Down Expand Up @@ -233,7 +238,7 @@
const execFileAsync = promisify(execFile);
const { stdout, stderr } = await execFileAsync(command, args, {
timeout,
shell: this.shell
shell: this.useShell ? this.shell : false
});

return { stdout, stderr };
Expand Down Expand Up @@ -310,7 +315,7 @@
const { stdout, stderr } = await execFileAsync(
pendingCommand.command,
pendingCommand.args,
{ shell: this.shell }
{ shell: this.useShell ? this.shell : false }
);

this.pendingCommands.delete(commandId);
Expand Down
12 changes: 10 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dxt_version": "0.1",
"name": "super-shell-mcp",
"version": "2.0.13",
"version": "2.0.15",
"description": "Execute shell commands across multiple platforms with built-in security controls",
"long_description": "An MCP server for executing shell commands on Windows, macOS, and Linux with automatic platform detection, command whitelisting, and approval workflows for security.",
"author": {
Expand All @@ -21,7 +21,8 @@
"command": "node",
"args": ["${__dirname}/server/index.js"],
"env": {
"CUSTOM_SHELL": "${user_config.shell_path}"
"CUSTOM_SHELL": "${user_config.shell_path}",
"SUPER_SHELL_USE_SHELL": "${user_config.enable_shell_mode}"
}
}
},
Expand Down Expand Up @@ -49,6 +50,13 @@
"title": "Custom Shell Path",
"description": "Optional: Specify a custom shell path (e.g., /bin/zsh, C:\\Windows\\System32\\cmd.exe)",
"required": false
},
"enable_shell_mode": {
"type": "boolean",
"title": "Enable Shell Parsing (unsafe)",
"description": "Set to true only if you fully trust the commands being executed.",
"required": false,
"default": false
}
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"name": "super-shell-mcp",
"version": "2.0.13",
"version": "2.0.15",
"description": "MCP server for executing shell commands across multiple platforms",
"type": "module",
"main": "build/index.js",
"bin": {
"super-shell-mcp": "./build/index.js"
"super-shell-mcp": "build/index.js"
},
"repository": {
"type": "git",
"url": "https://github.com/cfdude/super-shell-mcp.git"
"url": "git+https://github.com/cfdude/super-shell-mcp.git"
},
"files": [
"build"
Expand Down Expand Up @@ -54,4 +54,4 @@
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
}
62 changes: 56 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import * as path from 'path';
import * as fs from 'fs';

Check warning on line 12 in src/index.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

'fs' is defined but never used
import { execFile } from 'child_process';
import { promisify } from 'util';
import { randomUUID } from 'crypto';

Check warning on line 15 in src/index.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

'randomUUID' is defined but never used
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { CommandService, CommandSecurityLevel } from './services/command-service.js';
import { getLogger, Logger } from './utils/logger.js';

Check warning on line 19 in src/index.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

'Logger' is defined but never used

// In ESM, __dirname is not available directly, so we create it
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const execFileAsync = promisify(execFile);

Check warning on line 25 in src/index.ts

View workflow job for this annotation

GitHub Actions / lint-and-build

'execFileAsync' is assigned a value but never used
// Initialize the logger
// Use __dirname to get the directory of the current file
const LOG_FILE = path.join(__dirname, '../logs/super-shell-mcp.log');
Expand All @@ -32,21 +32,36 @@
/**
* SuperShellMcpServer - MCP server for executing shell commands across multiple platforms
*/
interface CommandExecutionOptions {
shell?: string;
useShell?: boolean;
defaultTimeout?: number;
}

interface SuperShellMcpServerOptions {
shell?: string;
commandExecution?: CommandExecutionOptions;
}

class SuperShellMcpServer {
private server: Server;
private commandService: CommandService;
private pendingApprovals: Map<string, { command: string; args: string[] }>;

constructor(options?: { shell?: string }) {
constructor(options?: SuperShellMcpServerOptions) {
// Initialize the command service with auto-detected or specified shell
this.commandService = new CommandService(options?.shell);
this.commandService = new CommandService({
shell: options?.commandExecution?.shell ?? options?.shell,
useShell: options?.commandExecution?.useShell,
defaultTimeout: options?.commandExecution?.defaultTimeout,
});
this.pendingApprovals = new Map();

// Initialize the MCP server
this.server = new Server(
{
name: 'super-shell-mcp',
version: '2.0.13',
version: '2.0.15',
},
{
capabilities: {
Expand Down Expand Up @@ -508,10 +523,11 @@
* Handle get_platform_info tool
*/
private async handleGetPlatformInfo() {
const { detectPlatform, getDefaultShell, getShellSuggestions, getCommonShellLocations } = await import('./utils/platform-utils.js');
const { detectPlatform, getShellSuggestions, getCommonShellLocations } = await import('./utils/platform-utils.js');

const platform = detectPlatform();
const currentShell = this.commandService.getShell();
const shellExecutionEnabled = this.commandService.isShellEnabled();
const suggestedShells = getShellSuggestions()[platform];
const commonLocations = getCommonShellLocations();

Expand All @@ -522,9 +538,12 @@
text: JSON.stringify({
platform,
currentShell,
shellExecutionEnabled,
suggestedShells,
commonLocations,
helpMessage: `Super Shell MCP is running on ${platform} using ${currentShell}`
helpMessage: shellExecutionEnabled
? `Super Shell MCP is running on ${platform} using ${currentShell} with shell parsing enabled.`
: `Super Shell MCP is running on ${platform} executing commands without shell parsing.`,
}, null, 2),
},
],
Expand Down Expand Up @@ -685,5 +704,36 @@
}

// Create and run the server
const server = new SuperShellMcpServer();
const customShellPath = process.env.CUSTOM_SHELL || process.env.SUPER_SHELL_SHELL_PATH;
const shellModeEnv = process.env.SUPER_SHELL_USE_SHELL || process.env.SUPER_SHELL_ENABLE_SHELL;
const commandTimeoutEnv = process.env.SUPER_SHELL_COMMAND_TIMEOUT;

const commandExecutionOptions: CommandExecutionOptions = {};

if (customShellPath) {
commandExecutionOptions.shell = customShellPath;
}

if (shellModeEnv !== undefined) {
commandExecutionOptions.useShell = /^(1|true|yes|on)$/i.test(shellModeEnv);
}

if (commandTimeoutEnv !== undefined) {
const parsedTimeout = Number(commandTimeoutEnv);
if (!Number.isNaN(parsedTimeout) && parsedTimeout > 0) {
commandExecutionOptions.defaultTimeout = parsedTimeout;
}
}

const serverOptions: SuperShellMcpServerOptions = {};

if (customShellPath) {
serverOptions.shell = customShellPath;
}

if (Object.keys(commandExecutionOptions).length > 0) {
serverOptions.commandExecution = commandExecutionOptions;
}

const server = new SuperShellMcpServer(serverOptions);
server.run().catch(console.error);
Loading
Loading