# Directory Structure ``` ├── .editorconfig ├── .gitignore ├── .prettierrc ├── bun.lock ├── cli.js ├── index.ts ├── package.json ├── README.md └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` { "semi": false } ``` -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- ``` # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = false insert_final_newline = false ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # dependencies (bun install) node_modules # output out dist *.tgz # code coverage coverage *.lcov # logs logs _.log report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # caches .eslintcache .cache *.tsbuildinfo # IntelliJ based IDEs .idea # Finder (MacOS) folder config .DS_Store ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # shell-command-mcp MCP server for executing shell commands. This project is sponsored by [ChatWise](https://chatwise.app), an all-in-one LLM chatbot with first-class MCP support. ## Usage ### Configure manually ```bash # stdio server npx -y shell-command-mcp ``` ### JSON config ```json { "mcpServers": { "shell-command": { "command": "npx", "args": ["-y", "shell-command-mcp"], "env": { "ALLOWED_COMMANDS": "cat,ls,echo" } } } } ``` ### Allowed commands Use `ALLOWED_COMMANDS` environment variable to explictly allow the commands that this server can run, separate each command by `,`. You can use `*` to allow any command, but this is potentially dangerous. ## License MIT. ``` -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node import "./dist/index.js" ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "shell-command-mcp", "description": "MCP server for running shell commands", "type": "module", "version": "0.0.1", "files": [ "dist", "/cli.js" ], "bin": "./cli.js", "scripts": { "build": "bun build ./index.ts --packages external --outdir dist", "prepublishOnly": "npm run build" }, "devDependencies": { "@types/bun": "latest", "@types/js-yaml": "^4.0.9", "typescript": "^5" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", "args-tokenizer": "^0.3.0", "js-yaml": "^4.1.0", "tinyexec": "^1.0.1", "zod": "^3.24.2" } } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { // Enable latest features "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false } } ``` -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z } from "zod" import { dump } from "js-yaml" import { version } from "./package.json" import { x } from "tinyexec" import { tokenizeArgs } from "args-tokenizer" const allowedCommands = process.env.ALLOWED_COMMANDS?.split(",").map((cmd) => cmd.trim()) || [] const server = new McpServer( { name: "shell-command-mcp", version, }, { capabilities: { logging: {}, tools: {}, }, } ) server.tool( "execute_command", "Execute a shell command", { command: z.string().describe("The shell command to execute"), }, async (args) => { const [bin, ...commandArgs] = tokenizeArgs(args.command) try { if (!allowedCommands.includes("*") && !allowedCommands.includes(bin)) { throw new Error( `Command "${bin}" is not allowed, allowed commands: ${ allowedCommands.length > 0 ? allowedCommands.join(", ") : "(none)" }` ) } const result = await x(bin, commandArgs) return { content: [ { type: "text", text: dump({ exit_code: result.exitCode, stdout: result.stdout, stderr: result.stderr, }), }, ], } } catch (error) { return { content: [ { type: "text", text: `Error executing command: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, } } } ) const transport = new StdioServerTransport() await server.connect(transport) ```