# Directory Structure ``` ├── .editorconfig ├── .gitignore ├── .prettierrc ├── bun.lock ├── cli.js ├── index.ts ├── package.json ├── README.md └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` 1 | { 2 | "semi": false 3 | } 4 | ``` -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- ``` 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # shell-command-mcp 2 | 3 | MCP server for executing shell commands. 4 | 5 | This project is sponsored by [ChatWise](https://chatwise.app), an all-in-one LLM chatbot with first-class MCP support. 6 | 7 | ## Usage 8 | 9 | ### Configure manually 10 | 11 | ```bash 12 | # stdio server 13 | npx -y shell-command-mcp 14 | ``` 15 | 16 | ### JSON config 17 | 18 | ```json 19 | { 20 | "mcpServers": { 21 | "shell-command": { 22 | "command": "npx", 23 | "args": ["-y", "shell-command-mcp"], 24 | "env": { 25 | "ALLOWED_COMMANDS": "cat,ls,echo" 26 | } 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | ### Allowed commands 33 | 34 | 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. 35 | 36 | ## License 37 | 38 | MIT. 39 | ``` -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | import "./dist/index.js" 3 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "shell-command-mcp", 3 | "description": "MCP server for running shell commands", 4 | "type": "module", 5 | "version": "0.0.1", 6 | "files": [ 7 | "dist", 8 | "/cli.js" 9 | ], 10 | "bin": "./cli.js", 11 | "scripts": { 12 | "build": "bun build ./index.ts --packages external --outdir dist", 13 | "prepublishOnly": "npm run build" 14 | }, 15 | "devDependencies": { 16 | "@types/bun": "latest", 17 | "@types/js-yaml": "^4.0.9", 18 | "typescript": "^5" 19 | }, 20 | "dependencies": { 21 | "@modelcontextprotocol/sdk": "^1.8.0", 22 | "args-tokenizer": "^0.3.0", 23 | "js-yaml": "^4.1.0", 24 | "tinyexec": "^1.0.1", 25 | "zod": "^3.24.2" 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" 3 | import { z } from "zod" 4 | import { dump } from "js-yaml" 5 | import { version } from "./package.json" 6 | import { x } from "tinyexec" 7 | import { tokenizeArgs } from "args-tokenizer" 8 | 9 | const allowedCommands = 10 | process.env.ALLOWED_COMMANDS?.split(",").map((cmd) => cmd.trim()) || [] 11 | 12 | const server = new McpServer( 13 | { 14 | name: "shell-command-mcp", 15 | version, 16 | }, 17 | { 18 | capabilities: { 19 | logging: {}, 20 | tools: {}, 21 | }, 22 | } 23 | ) 24 | 25 | server.tool( 26 | "execute_command", 27 | "Execute a shell command", 28 | { 29 | command: z.string().describe("The shell command to execute"), 30 | }, 31 | async (args) => { 32 | const [bin, ...commandArgs] = tokenizeArgs(args.command) 33 | 34 | try { 35 | if (!allowedCommands.includes("*") && !allowedCommands.includes(bin)) { 36 | throw new Error( 37 | `Command "${bin}" is not allowed, allowed commands: ${ 38 | allowedCommands.length > 0 ? allowedCommands.join(", ") : "(none)" 39 | }` 40 | ) 41 | } 42 | 43 | const result = await x(bin, commandArgs) 44 | 45 | return { 46 | content: [ 47 | { 48 | type: "text", 49 | text: dump({ 50 | exit_code: result.exitCode, 51 | stdout: result.stdout, 52 | stderr: result.stderr, 53 | }), 54 | }, 55 | ], 56 | } 57 | } catch (error) { 58 | return { 59 | content: [ 60 | { 61 | type: "text", 62 | text: `Error executing command: ${ 63 | error instanceof Error ? error.message : String(error) 64 | }`, 65 | }, 66 | ], 67 | isError: true, 68 | } 69 | } 70 | } 71 | ) 72 | 73 | const transport = new StdioServerTransport() 74 | await server.connect(transport) 75 | ```