#
tokens: 17267/50000 23/23 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .npmignore
├── config.json
├── Dockerfile
├── LICENSE
├── logo.png
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── sync-version.js
├── setup-claude-server.js
├── smithery.yaml
├── src
│   ├── command-manager.ts
│   ├── config.ts
│   ├── index.ts
│   ├── server.ts
│   ├── terminal-manager.ts
│   ├── tools
│   │   ├── edit.ts
│   │   ├── execute.ts
│   │   ├── filesystem.ts
│   │   ├── process.ts
│   │   └── schemas.ts
│   ├── types.ts
│   └── version.ts
├── test
│   └── test.js
├── testemonials
│   ├── img_1.png
│   ├── img_2.png
│   ├── img_3.png
│   ├── img_4.png
│   └── img.png
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
 1 | .git
 2 | .gitignore
 3 | .DS_Store
 4 | .history
 5 | .idea
 6 | src/
 7 | tsconfig.json
 8 | *.log
 9 | work/
10 | config.json
11 | setup-claude-server.js
```

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

```
 1 | # Dependencies
 2 | node_modules/
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | 
 7 | # TypeScript build output
 8 | dist/
 9 | *.tsbuildinfo
10 | 
11 | # IDE and editor files
12 | .idea/
13 | .vscode/
14 | *.swp
15 | *.swo
16 | .DS_Store
17 | 
18 | # Environment variables
19 | .env
20 | .env.local
21 | .env.*.local
22 | 
23 | # Logs
24 | logs/
25 | *.log
26 | 
27 | # Coverage directory used by tools like istanbul
28 | coverage/
29 | 
30 | # Optional npm cache directory
31 | .npm
32 | 
33 | # Optional eslint cache
34 | .eslintcache
35 | 
36 | .idea
37 | .history
38 | 
39 | server.log
```

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

```markdown
  1 | # Desktop Commander MCP
  2 | ![Desktop Commander MCP](logo.png)
  3 | 
  4 | [![npm downloads](https://img.shields.io/npm/dw/@wonderwhy-er/desktop-commander)](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
  5 | [![smithery badge](https://smithery.ai/badge/@wonderwhy-er/desktop-commander)](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
  6 | [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow.svg)](https://www.buymeacoffee.com/wonderwhyer)
  7 | [![Discord](https://img.shields.io/badge/Discord-Join%20Server-7289DA.svg)](https://discord.gg/7cbccwRp)
  8 | 
  9 | Short version. Two key things. Terminal commands and diff based file editing.
 10 | 
 11 | <a href="https://glama.ai/mcp/servers/zempur9oh4">
 12 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/zempur9oh4/badge" alt="Claude Desktop Commander MCP server" />
 13 | </a>
 14 | 
 15 | ## Table of Contents
 16 | - [Features](#features)
 17 | - [Installation](#installation)
 18 | - [Usage](#usage)
 19 | - [Handling Long-Running Commands](#handling-long-running-commands)
 20 | - [Work in Progress and TODOs](#work-in-progress-and-todos)
 21 | - [Media links](#media)
 22 | - [Testimonials](#testimonials)
 23 | - [Contributing](#contributing)
 24 | - [License](#license)
 25 | 
 26 | This is server that allows Claude desktop app to execute long-running terminal commands on your computer and manage processes through Model Context Protocol (MCP) + Built on top of [MCP Filesystem Server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) to provide additional search and replace file editing capabilities .
 27 | 
 28 | ## Features
 29 | 
 30 | - Execute terminal commands with output streaming
 31 | - Command timeout and background execution support
 32 | - Process management (list and kill processes)
 33 | - Session management for long-running commands
 34 | - Full filesystem operations:
 35 |   - Read/write files
 36 |   - Create/list directories
 37 |   - Move files/directories
 38 |   - Search files
 39 |   - Get file metadata
 40 |   - Code editing capabilities:
 41 |   - Surgical text replacements for small changes
 42 |   - Full file rewrites for major changes
 43 |   - Multiple file support
 44 |   - Pattern-based replacements
 45 | 
 46 | ## Installation
 47 | First, ensure you've downloaded and installed the [Claude Desktop app](https://claude.ai/download) and you have [npm installed](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
 48 | 
 49 | ### Option 1: Installing via Smithery
 50 | 
 51 | To install Desktop Commander for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@wonderwhy-er/desktop-commander):
 52 | 
 53 | ```bash
 54 | npx -y @smithery/cli install @wonderwhy-er/desktop-commander --client claude
 55 | ```
 56 | 
 57 | ### Option 2: Install trough npx
 58 | Just run this in terminal
 59 | ```
 60 | npx @wonderwhy-er/desktop-commander setup
 61 | ```
 62 | Restart Claude if running
 63 | 
 64 | ### Option 3: Add to claude_desktop_config by hand
 65 | Add this entry to your claude_desktop_config.json (on Mac, found at ~/Library/Application\ Support/Claude/claude_desktop_config.json):
 66 | ```json
 67 | {
 68 |   "mcpServers": {
 69 |     "desktop-commander": {
 70 |       "command": "npx",
 71 |       "args": [
 72 |         "-y",
 73 |         "@wonderwhy-er/desktop-commander"
 74 |       ]
 75 |     }
 76 |   }
 77 | }
 78 | ```
 79 | Restart Claude if running
 80 | 
 81 | ### Option 4: Checkout locally
 82 | 1. Clone and build:
 83 | ```bash
 84 | git clone https://github.com/wonderwhy-er/ClaudeComputerCommander.git
 85 | cd ClaudeComputerCommander
 86 | npm run setup
 87 | ```
 88 | Restart Claude if running
 89 | 
 90 | The setup command will:
 91 | - Install dependencies
 92 | - Build the server
 93 | - Configure Claude's desktop app
 94 | - Add MCP servers to Claude's config if needed
 95 | 
 96 | ## Usage
 97 | 
 98 | The server provides these tool categories:
 99 | 
100 | ### Terminal Tools
101 | - `execute_command`: Run commands with configurable timeout
102 | - `read_output`: Get output from long-running commands
103 | - `force_terminate`: Stop running command sessions
104 | - `list_sessions`: View active command sessions
105 | - `list_processes`: View system processes
106 | - `kill_process`: Terminate processes by PID
107 | - `block_command`/`unblock_command`: Manage command blacklist
108 | 
109 | ### Filesystem Tools
110 | - `read_file`/`write_file`: File operations
111 | - `create_directory`/`list_directory`: Directory management  
112 | - `move_file`: Move/rename files
113 | - `search_files`: Pattern-based file search
114 | - `get_file_info`: File metadata
115 | 
116 | ### Edit Tools
117 | - `edit_block`: Apply surgical text replacements (best for changes <20% of file size)
118 | - `write_file`: Complete file rewrites (best for large changes >20% or when edit_block fails)
119 | 
120 | Search/Replace Block Format:
121 | ```
122 | filepath.ext
123 | <<<<<<< SEARCH
124 | existing code to replace
125 | =======
126 | new code to insert
127 | >>>>>>> REPLACE
128 | ```
129 | 
130 | Example:
131 | ```
132 | src/main.js
133 | <<<<<<< SEARCH
134 | console.log("old message");
135 | =======
136 | console.log("new message");
137 | >>>>>>> REPLACE
138 | ```
139 | 
140 | ## Handling Long-Running Commands
141 | 
142 | For commands that may take a while:
143 | 
144 | 1. `execute_command` returns after timeout with initial output
145 | 2. Command continues in background
146 | 3. Use `read_output` with PID to get new output
147 | 4. Use `force_terminate` to stop if needed
148 | 
149 | ## Model Context Protocol Integration
150 | 
151 | This project extends the MCP Filesystem Server to enable:
152 | - Local server support in Claude Desktop
153 | - Full system command execution
154 | - Process management
155 | - File operations
156 | - Code editing with search/replace blocks
157 | 
158 | Created as part of exploring Claude MCPs: https://youtube.com/live/TlbjFDbl5Us
159 | 
160 | ## Work in Progress and TODOs
161 | 
162 | The following features are currently being developed or planned:
163 | 
164 | - **Better code search** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/17)) - Enhanced code exploration with context-aware results
165 | - **Better configurations** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/16)) - Improved settings for allowed paths, commands and shell environment
166 | - **Windows environment fixes** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/13)) - Resolving issues specific to Windows platforms
167 | - **Linux improvements** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/12)) - Enhancing compatibility with various Linux distributions
168 | - **Support for WSL** - Windows Subsystem for Linux integration
169 | - **Support for SSH** - Remote server command execution
170 | - **Installation troubleshooting guide** - Comprehensive help for setup issues
171 | 
172 | ## Media
173 | Learn more about this project through these resources:
174 | 
175 | ### Article
176 | [Claude with MCPs replaced Cursor & Windsurf. How did that happen?](https://wonderwhy-er.medium.com/claude-with-mcps-replaced-cursor-windsurf-how-did-that-happen-c1d1e2795e96) - A detailed exploration of how Claude with Model Context Protocol capabilities is changing developer workflows.
177 | 
178 | ### Video
179 | [Claude Desktop Commander Video Tutorial](https://www.youtube.com/watch?v=ly3bed99Dy8) - Watch how to set up and use the Commander effectively.
180 | 
181 | ### Community
182 | Join our [Discord server](https://discord.gg/7cbccwRp) to get help, share feedback, and connect with other users.
183 | 
184 | ## Testimonials
185 | 
186 | [![It's a life saver! I paid Claude + Cursor currently which I always feel it's kind of duplicated. This solves the problem ultimately. I am so happy. Thanks so much. Plus today Claude has added the web search support. With this MCP + Internet search, it writes the code with the latest updates. It's so good when Cursor doesn't work sometimes or all the fast requests are used.](testemonials/img.png) https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyyBt6_ShdDX_rIOad4AaABAg
187 | ](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyyBt6_ShdDX_rIOad4AaABAg
188 | )
189 | 
190 | [![This is the first comment I've ever left on a youtube video, THANK YOU! I've been struggling to update an old Flutter app in Cursor from an old pre null-safety version to a current version and implemented null-safety using Claude 3.7. I got most of the way but had critical BLE errors that I spent days trying to resolve with no luck. I tried Augment Code but it didn't get it either. I implemented your MCP in Claude desktop and was able to compare the old and new codebase fully, accounting for the updates in the code, and fix the issues in a couple of hours. A word of advice to people trying this, be sure to stage changes and commit when appropriate to be able to undo unwanted changes. Amazing!](testemonials/img_1.png)
191 | https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg
192 | )
193 | 
194 | [![Great! I just used Windsurf, bought license a week ago, for upgrading old fullstack socket project and it works many times good or ok but also many times runs away in cascade and have to revert all changes loosing hundereds of cascade tokens. In just a week down to less than 100 tokens and do not want to buy only 300 tokens for 10$. This Claude MCP ,bought claude Pro finally needed but wanted very good reason to also have next to ChatGPT, and now can code as much as I want not worrying about token cost.
195 | Also this is much more than code editing it is much more thank you for great video!](testemonials/img_2.png)
196 | https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg)
197 | 
198 | [![it is a great tool, thank you, I like using it, as it gives claude an ability to do surgical edits, making it more like a human developer.](testemonials/img_3.png)
199 | https://www.youtube.com/watch?v=ly3bed99Dy8&lc=Ugy4-exy166_Ma7TH-h4AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=Ugy4-exy166_Ma7TH-h4AaABAg)
200 | 
201 | [![You sir are my hero. You've pretty much summed up and described my experiences of late, much better than I could have. Cursor and Windsurf both had me frustrated to the point where I was almost yelling at my computer screen. Out of whimsy, I thought to myself why not just ask Claude directly, and haven't looked back since.
202 | Claude first to keep my sanity in check, then if necessary, engage with other IDEs, frameworks, etc. I thought I was the only one, glad to see I'm not lol.
203 | 33
204 | 1](testemonials/img_4.png)
205 | https://medium.com/@pharmx/you-sir-are-my-hero-62cff5836a3e](https://medium.com/@pharmx/you-sir-are-my-hero-62cff5836a3e)
206 | 
207 | ## Contributing
208 | 
209 | If you find this project useful, please consider giving it a ⭐ star on GitHub! This helps others discover the project and encourages further development.
210 | 
211 | We welcome contributions from the community! Whether you've found a bug, have a feature request, or want to contribute code, here's how you can help:
212 | 
213 | - **Found a bug?** Open an issue at [github.com/wonderwhy-er/ClaudeComputerCommander/issues](https://github.com/wonderwhy-er/ClaudeComputerCommander/issues)
214 | - **Have a feature idea?** Submit a feature request in the issues section
215 | - **Want to contribute code?** Fork the repository, create a branch, and submit a pull request
216 | - **Questions or discussions?** Start a discussion in the GitHub Discussions tab
217 | 
218 | All contributions, big or small, are greatly appreciated!
219 | 
220 | If you find this tool valuable for your workflow, please consider [supporting the project](https://www.buymeacoffee.com/wonderwhyer).
221 | 
222 | ## License
223 | 
224 | MIT
```

--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------

```typescript
1 | export const VERSION = '0.1.19';
2 | 
```

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

```json
 1 | {
 2 |   "blockedCommands": [
 3 |     "format",
 4 |     "mount",
 5 |     "umount",
 6 |     "mkfs",
 7 |     "fdisk",
 8 |     "dd",
 9 |     "sudo",
10 |     "su",
11 |     "passwd",
12 |     "adduser",
13 |     "useradd",
14 |     "usermod",
15 |     "groupadd"
16 |   ]
17 | }
```

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

```typescript
1 | import path from 'path';
2 | import process from 'process';
3 | 
4 | export const CONFIG_FILE = path.join(process.cwd(), 'config.json');
5 | export const LOG_FILE = path.join(process.cwd(), 'server.log');
6 | export const ERROR_LOG_FILE = path.join(process.cwd(), 'error.log');
7 | 
8 | export const DEFAULT_COMMAND_TIMEOUT = 1000; // milliseconds
9 | 
```

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

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     properties: {}
 9 |   commandFunction:
10 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
11 |     |-
12 |     (config) => ({ command: 'node', args: ['dist/index.js'] })
13 |   exampleConfig: {}
14 | 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "node16",
 6 |     "esModuleInterop": true,
 7 |     "strict": true,
 8 |     "outDir": "./dist",
 9 |     "rootDir": "./src",
10 |     "declaration": true,
11 |     "skipLibCheck": true,
12 |     "diagnostics": true,
13 |     "extendedDiagnostics": true,
14 |     "listEmittedFiles": true
15 |   },
16 |   "include": [
17 |     "src/**/*.ts"
18 |   ],
19 |   "exclude": [
20 |     "node_modules",
21 |     "dist"
22 |   ]
23 | }
```

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

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | # Create app directory
 5 | WORKDIR /usr/src/app
 6 | 
 7 | # Copy package.json and package-lock.json
 8 | COPY package*.json ./
 9 | 
10 | # Install dependencies without triggering any unwanted scripts
11 | RUN npm install --ignore-scripts
12 | 
13 | # Copy all source code
14 | COPY . .
15 | 
16 | # Build the application
17 | RUN npm run build
18 | 
19 | # Expose port if needed (not specified, so using none)
20 | 
21 | # Command to run the server
22 | CMD [ "node", "dist/index.js" ]
23 | 
```

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

```typescript
 1 | import { ChildProcess } from 'child_process';
 2 | 
 3 | export interface ProcessInfo {
 4 |   pid: number;
 5 |   command: string;
 6 |   cpu: string;
 7 |   memory: string;
 8 | }
 9 | 
10 | export interface TerminalSession {
11 |   pid: number;
12 |   process: ChildProcess;
13 |   lastOutput: string;
14 |   isBlocked: boolean;
15 |   startTime: Date;
16 | }
17 | 
18 | export interface CommandExecutionResult {
19 |   pid: number;
20 |   output: string;
21 |   isBlocked: boolean;
22 | }
23 | 
24 | export interface ActiveSession {
25 |   pid: number;
26 |   isBlocked: boolean;
27 |   runtime: number;
28 | }
29 | 
30 | export interface CompletedSession {
31 |   pid: number;
32 |   output: string;
33 |   exitCode: number | null;
34 |   startTime: Date;
35 |   endTime: Date;
36 | }
```

--------------------------------------------------------------------------------
/scripts/sync-version.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { readFileSync, writeFileSync } from 'fs';
 2 | import path from 'path';
 3 | 
 4 | function bumpVersion(version, type = 'patch') {
 5 |     const [major, minor, patch] = version.split('.').map(Number);
 6 |     switch(type) {
 7 |         case 'major':
 8 |             return `${major + 1}.0.0`;
 9 |         case 'minor':
10 |             return `${major}.${minor + 1}.0`;
11 |         case 'patch':
12 |         default:
13 |             return `${major}.${minor}.${patch + 1}`;
14 |     }
15 | }
16 | 
17 | // Read command line arguments
18 | const shouldBump = process.argv.includes('--bump');
19 | const bumpType = process.argv.includes('--major') ? 'major' 
20 |                : process.argv.includes('--minor') ? 'minor' 
21 |                : 'patch';
22 | 
23 | // Read version from package.json
24 | const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
25 | let version = pkg.version;
26 | 
27 | // Bump version if requested
28 | if (shouldBump) {
29 |     version = bumpVersion(version, bumpType);
30 |     // Update package.json
31 |     pkg.version = version;
32 |     writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
33 | }
34 | 
35 | // Update version.ts
36 | const versionFileContent = `export const VERSION = '${version}';\n`;
37 | writeFileSync('src/version.ts', versionFileContent);
38 | 
39 | console.log(`Version ${version} synchronized${shouldBump ? ' and bumped' : ''}`);
40 | 
```

--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { parseEditBlock, performSearchReplace } from '../dist/tools/edit.js';
 2 | 
 3 | async function runTests() {
 4 |     try {
 5 |         // Test parseEditBlock
 6 |         const testBlock = `test.txt
 7 | <<<<<<< SEARCH
 8 | old content
 9 | =======
10 | new content
11 | >>>>>>> REPLACE`;
12 | 
13 |         const parsed = await parseEditBlock(testBlock);
14 |         console.log('Parse test passed:', parsed);
15 | 
16 |         // Create a test file
17 |         const fs = await import('fs/promises');
18 |         const testFilePath = 'test/test.txt';
19 |         await fs.writeFile(testFilePath, 'This is old content to replace');
20 | 
21 |         // Test performSearchReplace
22 |         await performSearchReplace(testFilePath, {
23 |             search: 'old content',
24 |             replace: 'new content'
25 |         });
26 | 
27 |         const result = await fs.readFile(testFilePath, 'utf8');
28 |         console.log('File content after replacement:', result);
29 | 
30 |         if (result.includes('new content')) {
31 |             console.log('Replace test passed!');
32 |         } else {
33 |             throw new Error('Replace test failed!');
34 |         }
35 | 
36 |         // Cleanup
37 |         await fs.unlink(testFilePath);
38 |         console.log('All tests passed! 🎉');
39 |     } catch (error) {
40 |         console.error('Test failed:', error);
41 |         process.exit(1);
42 |     }
43 | }
44 | 
45 | runTests();
```

--------------------------------------------------------------------------------
/src/tools/schemas.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | 
 3 | // Terminal tools schemas
 4 | export const ExecuteCommandArgsSchema = z.object({
 5 |   command: z.string(),
 6 |   timeout_ms: z.number().optional(),
 7 | });
 8 | 
 9 | export const ReadOutputArgsSchema = z.object({
10 |   pid: z.number(),
11 | });
12 | 
13 | export const ForceTerminateArgsSchema = z.object({
14 |   pid: z.number(),
15 | });
16 | 
17 | export const ListSessionsArgsSchema = z.object({});
18 | 
19 | export const KillProcessArgsSchema = z.object({
20 |   pid: z.number(),
21 | });
22 | 
23 | export const BlockCommandArgsSchema = z.object({
24 |   command: z.string(),
25 | });
26 | 
27 | export const UnblockCommandArgsSchema = z.object({
28 |   command: z.string(),
29 | });
30 | 
31 | // Filesystem tools schemas
32 | export const ReadFileArgsSchema = z.object({
33 |   path: z.string(),
34 | });
35 | 
36 | export const ReadMultipleFilesArgsSchema = z.object({
37 |   paths: z.array(z.string()),
38 | });
39 | 
40 | export const WriteFileArgsSchema = z.object({
41 |   path: z.string(),
42 |   content: z.string(),
43 | });
44 | 
45 | export const CreateDirectoryArgsSchema = z.object({
46 |   path: z.string(),
47 | });
48 | 
49 | export const ListDirectoryArgsSchema = z.object({
50 |   path: z.string(),
51 | });
52 | 
53 | export const MoveFileArgsSchema = z.object({
54 |   source: z.string(),
55 |   destination: z.string(),
56 | });
57 | 
58 | export const SearchFilesArgsSchema = z.object({
59 |   path: z.string(),
60 |   pattern: z.string(),
61 | });
62 | 
63 | export const GetFileInfoArgsSchema = z.object({
64 |   path: z.string(),
65 | });
66 | 
67 | // Edit tools schemas
68 | export const EditBlockArgsSchema = z.object({
69 |   blockContent: z.string(),
70 | });
```

--------------------------------------------------------------------------------
/src/tools/edit.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { readFile, writeFile } from './filesystem.js';
 2 | 
 3 | interface SearchReplace {
 4 |     search: string;
 5 |     replace: string;
 6 | }
 7 | 
 8 | export async function performSearchReplace(filePath: string, block: SearchReplace): Promise<void> {
 9 |     const content = await readFile(filePath);
10 |     
11 |     // Find first occurrence
12 |     const searchIndex = content.indexOf(block.search);
13 |     if (searchIndex === -1) {
14 |         throw new Error(`Search content not found in ${filePath}`);
15 |     }
16 | 
17 |     // Replace content
18 |     const newContent = 
19 |         content.substring(0, searchIndex) + 
20 |         block.replace + 
21 |         content.substring(searchIndex + block.search.length);
22 | 
23 |     await writeFile(filePath, newContent);
24 | }
25 | 
26 | export async function parseEditBlock(blockContent: string): Promise<{
27 |     filePath: string;
28 |     searchReplace: SearchReplace;
29 | }> {
30 |     const lines = blockContent.split('\n');
31 |     
32 |     // First line should be the file path
33 |     const filePath = lines[0].trim();
34 |     
35 |     // Find the markers
36 |     const searchStart = lines.indexOf('<<<<<<< SEARCH');
37 |     const divider = lines.indexOf('=======');
38 |     const replaceEnd = lines.indexOf('>>>>>>> REPLACE');
39 |     
40 |     if (searchStart === -1 || divider === -1 || replaceEnd === -1) {
41 |         throw new Error('Invalid edit block format - missing markers');
42 |     }
43 |     
44 |     // Extract search and replace content
45 |     const search = lines.slice(searchStart + 1, divider).join('\n');
46 |     const replace = lines.slice(divider + 1, replaceEnd).join('\n');
47 |     
48 |     return {
49 |         filePath,
50 |         searchReplace: { search, replace }
51 |     };
52 | }
```

--------------------------------------------------------------------------------
/src/tools/process.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { exec } from 'child_process';
 2 | import { promisify } from 'util';
 3 | import os from 'os';
 4 | import { ProcessInfo } from '../types.js';
 5 | import { KillProcessArgsSchema } from './schemas.js';
 6 | 
 7 | const execAsync = promisify(exec);
 8 | 
 9 | export async function listProcesses(): Promise<{content: Array<{type: string, text: string}>}> {
10 |   const command = os.platform() === 'win32' ? 'tasklist' : 'ps aux';
11 |   try {
12 |     const { stdout } = await execAsync(command);
13 |     const processes = stdout.split('\n')
14 |       .slice(1)
15 |       .filter(Boolean)
16 |       .map(line => {
17 |         const parts = line.split(/\s+/);
18 |         return {
19 |           pid: parseInt(parts[1]),
20 |           command: parts[parts.length - 1],
21 |           cpu: parts[2],
22 |           memory: parts[3],
23 |         } as ProcessInfo;
24 |       });
25 | 
26 |     return {
27 |       content: [{
28 |         type: "text",
29 |         text: processes.map(p =>
30 |           `PID: ${p.pid}, Command: ${p.command}, CPU: ${p.cpu}, Memory: ${p.memory}`
31 |         ).join('\n')
32 |       }],
33 |     };
34 |   } catch (error) {
35 |     throw new Error('Failed to list processes');
36 |   }
37 | }
38 | 
39 | export async function killProcess(args: unknown) {
40 | 
41 |   const parsed = KillProcessArgsSchema.safeParse(args);
42 |   if (!parsed.success) {
43 |     throw new Error(`Invalid arguments for kill_process: ${parsed.error}`);
44 |   }
45 | 
46 |   try {
47 |     process.kill(parsed.data.pid);
48 |     return {
49 |       content: [{ type: "text", text: `Successfully terminated process ${parsed.data.pid}` }],
50 |     };
51 |   } catch (error) {
52 |     throw new Error(`Failed to kill process: ${error instanceof Error ? error.message : String(error)}`);
53 |   }
54 | }
55 | 
```

--------------------------------------------------------------------------------
/src/command-manager.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import fs from 'fs/promises';
 2 | import { CONFIG_FILE } from './config.js';
 3 | 
 4 | class CommandManager {
 5 |   private blockedCommands: Set<string> = new Set();
 6 | 
 7 |   async loadBlockedCommands(): Promise<void> {
 8 |     try {
 9 |       const configData = await fs.readFile(CONFIG_FILE, 'utf-8');
10 |       const config = JSON.parse(configData);
11 |       this.blockedCommands = new Set(config.blockedCommands);
12 |     } catch (error) {
13 |       this.blockedCommands = new Set();
14 |     }
15 |   }
16 | 
17 |   async saveBlockedCommands(): Promise<void> {
18 |     try {
19 |       const config = {
20 |         blockedCommands: Array.from(this.blockedCommands)
21 |       };
22 |       await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
23 |     } catch (error) {
24 |       // Handle error if needed
25 |     }
26 |   }
27 | 
28 |   validateCommand(command: string): boolean {
29 |     const baseCommand = command.split(' ')[0].toLowerCase().trim();
30 |     return !this.blockedCommands.has(baseCommand);
31 |   }
32 | 
33 |   async blockCommand(command: string): Promise<boolean> {
34 |     command = command.toLowerCase().trim();
35 |     if (this.blockedCommands.has(command)) {
36 |       return false;
37 |     }
38 |     this.blockedCommands.add(command);
39 |     await this.saveBlockedCommands();
40 |     return true;
41 |   }
42 | 
43 |   async unblockCommand(command: string): Promise<boolean> {
44 |     command = command.toLowerCase().trim();
45 |     if (!this.blockedCommands.has(command)) {
46 |       return false;
47 |     }
48 |     this.blockedCommands.delete(command);
49 |     await this.saveBlockedCommands();
50 |     return true;
51 |   }
52 | 
53 |   listBlockedCommands(): string[] {
54 |     return Array.from(this.blockedCommands).sort();
55 |   }
56 | }
57 | 
58 | export const commandManager = new CommandManager();
59 | 
```

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

```json
 1 | {
 2 |   "name": "@wonderwhy-er/desktop-commander",
 3 |   "version": "0.1.19",
 4 |   "description": "MCP server for terminal operations and file editing",
 5 |   "license": "MIT",
 6 |   "author": "Eduards Ruzga",
 7 |   "homepage": "https://github.com/wonderwhy-er/ClaudeComputerCommander",
 8 |   "bugs": "https://github.com/wonderwhy-er/ClaudeComputerCommander/issues",
 9 |   "type": "module",
10 |   "bin": {
11 |     "desktop-commander": "dist/index.js",
12 |     "setup": "dist/setup-claude-server.js"
13 |   },
14 |   "files": [
15 |     "dist"
16 |   ],
17 |   "scripts": {
18 |     "sync-version": "node scripts/sync-version.js",
19 |     "bump": "node scripts/sync-version.js --bump",
20 |     "bump:minor": "node scripts/sync-version.js --bump --minor",
21 |     "bump:major": "node scripts/sync-version.js --bump --major",
22 |     "build": "tsc && shx cp setup-claude-server.js dist/ && shx chmod +x dist/*.js",
23 |     "watch": "tsc --watch",
24 |     "start": "node dist/index.js",
25 |     "setup": "npm install && npm run build && node setup-claude-server.js",
26 |     "prepare": "npm run build",
27 |     "test": "node test/test.js",
28 |     "test:watch": "nodemon test/test.js",
29 |     "link:local": "npm run build && npm link",
30 |     "unlink:local": "npm unlink",
31 |     "inspector": "npx @modelcontextprotocol/inspector dist/index.js"
32 |   },
33 |   "publishConfig": {
34 |     "access": "public"
35 |   },
36 |   "keywords": [
37 |     "mcp",
38 |     "model-context-protocol",
39 |     "terminal",
40 |     "claude",
41 |     "ai",
42 |     "command-line",
43 |     "process-management",
44 |     "file-editing",
45 |     "code-editing",
46 |     "diff",
47 |     "patch",
48 |     "block-editing",
49 |     "file-system",
50 |     "text-manipulation",
51 |     "code-modification",
52 |     "surgical-edits",
53 |     "file-operations"
54 |   ],
55 |   "dependencies": {
56 |     "@modelcontextprotocol/sdk": "1.0.1",
57 |     "glob": "^10.3.10",
58 |     "zod": "^3.24.1",
59 |     "zod-to-json-schema": "^3.23.5"
60 |   },
61 |   "devDependencies": {
62 |     "@types/node": "^20.11.0",
63 |     "nodemon": "^3.0.2",
64 |     "shx": "^0.3.4",
65 |     "typescript": "^5.3.3"
66 |   }
67 | }
68 | 
```

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

```typescript
 1 | #!/usr/bin/env node
 2 | 
 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 4 | import { server } from './server.js';
 5 | import { commandManager } from './command-manager.js';
 6 | import { join, dirname } from 'path';
 7 | import { fileURLToPath } from 'url';
 8 | 
 9 | const __filename = fileURLToPath(import.meta.url);
10 | const __dirname = dirname(__filename);
11 | 
12 | async function runSetup() {
13 |   const setupScript = join(__dirname, 'setup-claude-server.js');
14 |   const { default: setupModule } = await import(setupScript);
15 |   if (typeof setupModule === 'function') {
16 |     await setupModule();
17 |   }
18 | }
19 | 
20 | async function runServer() {
21 |   try {
22 |     // Check if first argument is "setup"
23 |     if (process.argv[2] === 'setup') {
24 |       await runSetup();
25 |       return;
26 |     }
27 |     
28 |     // Handle uncaught exceptions
29 |     process.on('uncaughtException', async (error) => {
30 |       const errorMessage = error instanceof Error ? error.message : String(error);
31 |       process.exit(1);
32 |     });
33 | 
34 |     // Handle unhandled rejections
35 |     process.on('unhandledRejection', async (reason) => {
36 |       const errorMessage = reason instanceof Error ? reason.message : String(reason);
37 |       process.exit(1);
38 |     });
39 | 
40 |     const transport = new StdioServerTransport();
41 |     
42 |     // Load blocked commands from config file
43 |     await commandManager.loadBlockedCommands();
44 | 
45 |     await server.connect(transport);
46 |   } catch (error) {
47 |     const errorMessage = error instanceof Error ? error.message : String(error);
48 |     process.stderr.write(JSON.stringify({
49 |       type: 'error',
50 |       timestamp: new Date().toISOString(),
51 |       message: `Failed to start server: ${errorMessage}`
52 |     }) + '\n');
53 |     process.exit(1);
54 |   }
55 | }
56 | 
57 | runServer().catch(async (error) => {
58 |   const errorMessage = error instanceof Error ? error.message : String(error);
59 |   process.stderr.write(JSON.stringify({
60 |     type: 'error',
61 |     timestamp: new Date().toISOString(),
62 |     message: `Fatal error running server: ${errorMessage}`
63 |   }) + '\n');
64 |   process.exit(1);
65 | });
```

--------------------------------------------------------------------------------
/src/tools/execute.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { terminalManager } from '../terminal-manager.js';
 2 | import { commandManager } from '../command-manager.js';
 3 | import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema } from './schemas.js';
 4 | 
 5 | export async function executeCommand(args: unknown) {
 6 |   const parsed = ExecuteCommandArgsSchema.safeParse(args);
 7 |   if (!parsed.success) {
 8 |     throw new Error(`Invalid arguments for execute_command: ${parsed.error}`);
 9 |   }
10 | 
11 |   if (!commandManager.validateCommand(parsed.data.command)) {
12 |     throw new Error(`Command not allowed: ${parsed.data.command}`);
13 |   }
14 | 
15 |   const result = await terminalManager.executeCommand(
16 |     parsed.data.command,
17 |     parsed.data.timeout_ms
18 |   );
19 | 
20 |   return {
21 |     content: [{
22 |       type: "text",
23 |       text: `Command started with PID ${result.pid}\nInitial output:\n${result.output}${
24 |         result.isBlocked ? '\nCommand is still running. Use read_output to get more output.' : ''
25 |       }`
26 |     }],
27 |   };
28 | }
29 | 
30 | export async function readOutput(args: unknown) {
31 |   const parsed = ReadOutputArgsSchema.safeParse(args);
32 |   if (!parsed.success) {
33 |     throw new Error(`Invalid arguments for read_output: ${parsed.error}`);
34 |   }
35 | 
36 |   const output = terminalManager.getNewOutput(parsed.data.pid);
37 |   return {
38 |     content: [{
39 |       type: "text",
40 |       text: output === null
41 |         ? `No session found for PID ${parsed.data.pid}`
42 |         : output || 'No new output available'
43 |     }],
44 |   };
45 | }
46 | 
47 | export async function forceTerminate(args: unknown) {
48 |   const parsed = ForceTerminateArgsSchema.safeParse(args);
49 |   if (!parsed.success) {
50 |     throw new Error(`Invalid arguments for force_terminate: ${parsed.error}`);
51 |   }
52 | 
53 |   const success = terminalManager.forceTerminate(parsed.data.pid);
54 |   return {
55 |     content: [{
56 |       type: "text",
57 |       text: success
58 |         ? `Successfully initiated termination of session ${parsed.data.pid}`
59 |         : `No active session found for PID ${parsed.data.pid}`
60 |     }],
61 |   };
62 | }
63 | 
64 | export async function listSessions() {
65 |   const sessions = terminalManager.listActiveSessions();
66 |   return {
67 |     content: [{
68 |       type: "text",
69 |       text: sessions.length === 0
70 |         ? 'No active sessions'
71 |         : sessions.map(s =>
72 |             `PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s`
73 |           ).join('\n')
74 |     }],
75 |   };
76 | }
77 | 
```

--------------------------------------------------------------------------------
/setup-claude-server.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { homedir, platform } from 'os';
  2 | import { join } from 'path';
  3 | import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
  4 | import { fileURLToPath } from 'url';
  5 | import { dirname } from 'path';
  6 | 
  7 | const __filename = fileURLToPath(import.meta.url);
  8 | const __dirname = dirname(__filename);
  9 | 
 10 | // Determine OS and set appropriate config path and command
 11 | const isWindows = platform() === 'win32';
 12 | const claudeConfigPath = isWindows
 13 |     ? join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json')
 14 |     : join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
 15 | 
 16 | // Setup logging
 17 | const LOG_FILE = join(__dirname, 'setup.log');
 18 | 
 19 | function logToFile(message, isError = false) {
 20 |     const timestamp = new Date().toISOString();
 21 |     const logMessage = `${timestamp} - ${isError ? 'ERROR: ' : ''}${message}\n`;
 22 |     try {
 23 |         appendFileSync(LOG_FILE, logMessage);
 24 |         // For setup script, we'll still output to console but in JSON format
 25 |         const jsonOutput = {
 26 |             type: isError ? 'error' : 'info',
 27 |             timestamp,
 28 |             message
 29 |         };
 30 |         process.stdout.write(JSON.stringify(jsonOutput) + '\n');
 31 |     } catch (err) {
 32 |         // Last resort error handling
 33 |         process.stderr.write(JSON.stringify({
 34 |             type: 'error',
 35 |             timestamp: new Date().toISOString(),
 36 |             message: `Failed to write to log file: ${err.message}`
 37 |         }) + '\n');
 38 |     }
 39 | }
 40 | 
 41 | // Check if config file exists and create default if not
 42 | if (!existsSync(claudeConfigPath)) {
 43 |     logToFile(`Claude config file not found at: ${claudeConfigPath}`);
 44 |     logToFile('Creating default config file...');
 45 |     
 46 |     // Create the directory if it doesn't exist
 47 |     const configDir = dirname(claudeConfigPath);
 48 |     if (!existsSync(configDir)) {
 49 |         import('fs').then(fs => fs.mkdirSync(configDir, { recursive: true }));
 50 |     }
 51 |     
 52 |     // Create default config
 53 |     const defaultConfig = {
 54 |         "serverConfig": isWindows
 55 |             ? {
 56 |                 "command": "cmd.exe",
 57 |                 "args": ["/c"]
 58 |               }
 59 |             : {
 60 |                 "command": "/bin/sh",
 61 |                 "args": ["-c"]
 62 |               }
 63 |     };
 64 |     
 65 |     writeFileSync(claudeConfigPath, JSON.stringify(defaultConfig, null, 2));
 66 |     logToFile('Default config file created. Please update it with your Claude API credentials.');
 67 | }
 68 | 
 69 | try {
 70 |     // Read existing config
 71 |     const configData = readFileSync(claudeConfigPath, 'utf8');
 72 |     const config = JSON.parse(configData);
 73 | 
 74 |     // Prepare the new server config based on OS
 75 |     // Determine if running through npx or locally
 76 |     const isNpx =  import.meta.url.endsWith('dist/setup-claude-server.js');
 77 | 
 78 |     const serverConfig = isNpx ? {
 79 |         "command": "npx",
 80 |         "args": [
 81 |             "@wonderwhy-er/desktop-commander"
 82 |         ]
 83 |     } : {
 84 |         "command": "node",
 85 |         "args": [
 86 |             join(__dirname, 'dist', 'index.js')
 87 |         ]
 88 |     };
 89 | 
 90 |     // Add or update the terminal server config
 91 |     if (!config.mcpServers) {
 92 |         config.mcpServers = {};
 93 |     }
 94 |     
 95 |     config.mcpServers.desktopCommander = serverConfig;
 96 | 
 97 |     // Add puppeteer server if not present
 98 |     /*if (!config.mcpServers.puppeteer) {
 99 |         config.mcpServers.puppeteer = {
100 |             "command": "npx",
101 |             "args": ["-y", "@modelcontextprotocol/server-puppeteer"]
102 |         };
103 |     }*/
104 | 
105 |     // Write the updated config back
106 |     writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), 'utf8');
107 |     
108 |     logToFile('Successfully added MCP servers to Claude configuration!');
109 |     logToFile(`Configuration location: ${claudeConfigPath}`);
110 |     logToFile('\nTo use the servers:\n1. Restart Claude if it\'s currently running\n2. The servers will be available in Claude\'s MCP server list');
111 |     
112 | } catch (error) {
113 |     logToFile(`Error updating Claude configuration: ${error}`, true);
114 |     process.exit(1);
115 | }
```

--------------------------------------------------------------------------------
/src/terminal-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { spawn } from 'child_process';
  2 | import { TerminalSession, CommandExecutionResult, ActiveSession } from './types.js';
  3 | import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
  4 | 
  5 | interface CompletedSession {
  6 |   pid: number;
  7 |   output: string;
  8 |   exitCode: number | null;
  9 |   startTime: Date;
 10 |   endTime: Date;
 11 | }
 12 | 
 13 | export class TerminalManager {
 14 |   private sessions: Map<number, TerminalSession> = new Map();
 15 |   private completedSessions: Map<number, CompletedSession> = new Map();
 16 |   
 17 |   async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT): Promise<CommandExecutionResult> {
 18 |     const process = spawn(command, [], { shell: true });
 19 |     let output = '';
 20 |     
 21 |     // Ensure process.pid is defined before proceeding
 22 |     if (!process.pid) {
 23 |       throw new Error('Failed to get process ID');
 24 |     }
 25 |     
 26 |     const session: TerminalSession = {
 27 |       pid: process.pid,
 28 |       process,
 29 |       lastOutput: '',
 30 |       isBlocked: false,
 31 |       startTime: new Date()
 32 |     };
 33 |     
 34 |     this.sessions.set(process.pid, session);
 35 | 
 36 |     return new Promise((resolve) => {
 37 |       process.stdout.on('data', (data) => {
 38 |         const text = data.toString();
 39 |         output += text;
 40 |         session.lastOutput += text;
 41 |       });
 42 | 
 43 |       process.stderr.on('data', (data) => {
 44 |         const text = data.toString();
 45 |         output += text;
 46 |         session.lastOutput += text;
 47 |       });
 48 | 
 49 |       setTimeout(() => {
 50 |         session.isBlocked = true;
 51 |         resolve({
 52 |           pid: process.pid!,
 53 |           output,
 54 |           isBlocked: true
 55 |         });
 56 |       }, timeoutMs);
 57 | 
 58 |       process.on('exit', (code) => {
 59 |         if (process.pid) {
 60 |           // Store completed session before removing active session
 61 |           this.completedSessions.set(process.pid, {
 62 |             pid: process.pid,
 63 |             output: output + session.lastOutput, // Combine all output
 64 |             exitCode: code,
 65 |             startTime: session.startTime,
 66 |             endTime: new Date()
 67 |           });
 68 |           
 69 |           // Keep only last 100 completed sessions
 70 |           if (this.completedSessions.size > 100) {
 71 |             const oldestKey = Array.from(this.completedSessions.keys())[0];
 72 |             this.completedSessions.delete(oldestKey);
 73 |           }
 74 |           
 75 |           this.sessions.delete(process.pid);
 76 |         }
 77 |         resolve({
 78 |           pid: process.pid!,
 79 |           output,
 80 |           isBlocked: false
 81 |         });
 82 |       });
 83 |     });
 84 |   }
 85 | 
 86 |   getNewOutput(pid: number): string | null {
 87 |     // First check active sessions
 88 |     const session = this.sessions.get(pid);
 89 |     if (session) {
 90 |       const output = session.lastOutput;
 91 |       session.lastOutput = '';
 92 |       return output;
 93 |     }
 94 | 
 95 |     // Then check completed sessions
 96 |     const completedSession = this.completedSessions.get(pid);
 97 |     if (completedSession) {
 98 |       // Format completion message with exit code and runtime
 99 |       const runtime = (completedSession.endTime.getTime() - completedSession.startTime.getTime()) / 1000;
100 |       return `Process completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s\nFinal output:\n${completedSession.output}`;
101 |     }
102 | 
103 |     return null;
104 |   }
105 | 
106 |   forceTerminate(pid: number): boolean {
107 |     const session = this.sessions.get(pid);
108 |     if (!session) {
109 |       return false;
110 |     }
111 | 
112 |     try {
113 |       session.process.kill('SIGINT');
114 |       setTimeout(() => {
115 |         if (this.sessions.has(pid)) {
116 |           session.process.kill('SIGKILL');
117 |         }
118 |       }, 1000);
119 |       return true;
120 |     } catch (error) {
121 |       console.error(`Failed to terminate process ${pid}:`, error);
122 |       return false;
123 |     }
124 |   }
125 | 
126 |   listActiveSessions(): ActiveSession[] {
127 |     const now = new Date();
128 |     return Array.from(this.sessions.values()).map(session => ({
129 |       pid: session.pid,
130 |       isBlocked: session.isBlocked,
131 |       runtime: now.getTime() - session.startTime.getTime()
132 |     }));
133 |   }
134 | 
135 |   listCompletedSessions(): CompletedSession[] {
136 |     return Array.from(this.completedSessions.values());
137 |   }
138 | }
139 | 
140 | export const terminalManager = new TerminalManager();
```

--------------------------------------------------------------------------------
/src/tools/filesystem.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fs from "fs/promises";
  2 | import path from "path";
  3 | import os from 'os';
  4 | 
  5 | // Store allowed directories
  6 | const allowedDirectories: string[] = [
  7 |     process.cwd(), // Current working directory
  8 |     os.homedir()   // User's home directory
  9 | ];
 10 | 
 11 | // Normalize all paths consistently
 12 | function normalizePath(p: string): string {
 13 |     return path.normalize(p).toLowerCase();
 14 | }
 15 | 
 16 | function expandHome(filepath: string): string {
 17 |     if (filepath.startsWith('~/') || filepath === '~') {
 18 |         return path.join(os.homedir(), filepath.slice(1));
 19 |     }
 20 |     return filepath;
 21 | }
 22 | 
 23 | // Security utilities
 24 | export async function validatePath(requestedPath: string): Promise<string> {
 25 |     const expandedPath = expandHome(requestedPath);
 26 |     const absolute = path.isAbsolute(expandedPath)
 27 |         ? path.resolve(expandedPath)
 28 |         : path.resolve(process.cwd(), expandedPath);
 29 |         
 30 |     const normalizedRequested = normalizePath(absolute);
 31 | 
 32 |     // Check if path is within allowed directories
 33 |     const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(normalizePath(dir)));
 34 |     if (!isAllowed) {
 35 |         throw new Error(`Access denied - path outside allowed directories: ${absolute}`);
 36 |     }
 37 | 
 38 |     // Handle symlinks by checking their real path
 39 |     try {
 40 |         const realPath = await fs.realpath(absolute);
 41 |         const normalizedReal = normalizePath(realPath);
 42 |         const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(normalizePath(dir)));
 43 |         if (!isRealPathAllowed) {
 44 |             throw new Error("Access denied - symlink target outside allowed directories");
 45 |         }
 46 |         return realPath;
 47 |     } catch (error) {
 48 |         // For new files that don't exist yet, verify parent directory
 49 |         const parentDir = path.dirname(absolute);
 50 |         try {
 51 |             const realParentPath = await fs.realpath(parentDir);
 52 |             const normalizedParent = normalizePath(realParentPath);
 53 |             const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(normalizePath(dir)));
 54 |             if (!isParentAllowed) {
 55 |                 throw new Error("Access denied - parent directory outside allowed directories");
 56 |             }
 57 |             return absolute;
 58 |         } catch {
 59 |             throw new Error(`Parent directory does not exist: ${parentDir}`);
 60 |         }
 61 |     }
 62 | }
 63 | 
 64 | // File operation tools
 65 | export async function readFile(filePath: string): Promise<string> {
 66 |     const validPath = await validatePath(filePath);
 67 |     return fs.readFile(validPath, "utf-8");
 68 | }
 69 | 
 70 | export async function writeFile(filePath: string, content: string): Promise<void> {
 71 |     const validPath = await validatePath(filePath);
 72 |     await fs.writeFile(validPath, content, "utf-8");
 73 | }
 74 | 
 75 | export async function readMultipleFiles(paths: string[]): Promise<string[]> {
 76 |     return Promise.all(
 77 |         paths.map(async (filePath: string) => {
 78 |             try {
 79 |                 const validPath = await validatePath(filePath);
 80 |                 const content = await fs.readFile(validPath, "utf-8");
 81 |                 return `${filePath}:\n${content}\n`;
 82 |             } catch (error) {
 83 |                 const errorMessage = error instanceof Error ? error.message : String(error);
 84 |                 return `${filePath}: Error - ${errorMessage}`;
 85 |             }
 86 |         }),
 87 |     );
 88 | }
 89 | 
 90 | export async function createDirectory(dirPath: string): Promise<void> {
 91 |     const validPath = await validatePath(dirPath);
 92 |     await fs.mkdir(validPath, { recursive: true });
 93 | }
 94 | 
 95 | export async function listDirectory(dirPath: string): Promise<string[]> {
 96 |     const validPath = await validatePath(dirPath);
 97 |     const entries = await fs.readdir(validPath, { withFileTypes: true });
 98 |     return entries.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`);
 99 | }
100 | 
101 | export async function moveFile(sourcePath: string, destinationPath: string): Promise<void> {
102 |     const validSourcePath = await validatePath(sourcePath);
103 |     const validDestPath = await validatePath(destinationPath);
104 |     await fs.rename(validSourcePath, validDestPath);
105 | }
106 | 
107 | export async function searchFiles(rootPath: string, pattern: string): Promise<string[]> {
108 |     const results: string[] = [];
109 | 
110 |     async function search(currentPath: string) {
111 |         const entries = await fs.readdir(currentPath, { withFileTypes: true });
112 | 
113 |         for (const entry of entries) {
114 |             const fullPath = path.join(currentPath, entry.name);
115 |             
116 |             try {
117 |                 await validatePath(fullPath);
118 | 
119 |                 if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
120 |                     results.push(fullPath);
121 |                 }
122 | 
123 |                 if (entry.isDirectory()) {
124 |                     await search(fullPath);
125 |                 }
126 |             } catch (error) {
127 |                 continue;
128 |             }
129 |         }
130 |     }
131 | 
132 |     const validPath = await validatePath(rootPath);
133 |     await search(validPath);
134 |     return results;
135 | }
136 | 
137 | export async function getFileInfo(filePath: string): Promise<Record<string, any>> {
138 |     const validPath = await validatePath(filePath);
139 |     const stats = await fs.stat(validPath);
140 |     
141 |     return {
142 |         size: stats.size,
143 |         created: stats.birthtime,
144 |         modified: stats.mtime,
145 |         accessed: stats.atime,
146 |         isDirectory: stats.isDirectory(),
147 |         isFile: stats.isFile(),
148 |         permissions: stats.mode.toString(8).slice(-3),
149 |     };
150 | }
151 | 
152 | export function listAllowedDirectories(): string[] {
153 |     return allowedDirectories;
154 | }
155 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  2 | import {
  3 |   CallToolRequestSchema,
  4 |   ListToolsRequestSchema,
  5 |   type CallToolRequest,
  6 | } from "@modelcontextprotocol/sdk/types.js";
  7 | import { zodToJsonSchema } from "zod-to-json-schema";
  8 | import { commandManager } from './command-manager.js';
  9 | import {
 10 |   ExecuteCommandArgsSchema,
 11 |   ReadOutputArgsSchema,
 12 |   ForceTerminateArgsSchema,
 13 |   ListSessionsArgsSchema,
 14 |   KillProcessArgsSchema,
 15 |   BlockCommandArgsSchema,
 16 |   UnblockCommandArgsSchema,
 17 |   ReadFileArgsSchema,
 18 |   ReadMultipleFilesArgsSchema,
 19 |   WriteFileArgsSchema,
 20 |   CreateDirectoryArgsSchema,
 21 |   ListDirectoryArgsSchema,
 22 |   MoveFileArgsSchema,
 23 |   SearchFilesArgsSchema,
 24 |   GetFileInfoArgsSchema,
 25 |   EditBlockArgsSchema,
 26 | } from './tools/schemas.js';
 27 | import { executeCommand, readOutput, forceTerminate, listSessions } from './tools/execute.js';
 28 | import { listProcesses, killProcess } from './tools/process.js';
 29 | import {
 30 |   readFile,
 31 |   readMultipleFiles,
 32 |   writeFile,
 33 |   createDirectory,
 34 |   listDirectory,
 35 |   moveFile,
 36 |   searchFiles,
 37 |   getFileInfo,
 38 |   listAllowedDirectories,
 39 | } from './tools/filesystem.js';
 40 | import { parseEditBlock, performSearchReplace } from './tools/edit.js';
 41 | 
 42 | import { VERSION } from './version.js';
 43 | 
 44 | export const server = new Server(
 45 |   {
 46 |     name: "desktop-commander",
 47 |     version: VERSION,
 48 |   },
 49 |   {
 50 |     capabilities: {
 51 |       tools: {},
 52 |     },
 53 |   },
 54 | );
 55 | 
 56 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 57 |   return {
 58 |     tools: [
 59 |       // Terminal tools
 60 |       {
 61 |         name: "execute_command",
 62 |         description:
 63 |           "Execute a terminal command with timeout. Command will continue running in background if it doesn't complete within timeout.",
 64 |         inputSchema: zodToJsonSchema(ExecuteCommandArgsSchema),
 65 |       },
 66 |       {
 67 |         name: "read_output",
 68 |         description:
 69 |           "Read new output from a running terminal session.",
 70 |         inputSchema: zodToJsonSchema(ReadOutputArgsSchema),
 71 |       },
 72 |       {
 73 |         name: "force_terminate",
 74 |         description:
 75 |           "Force terminate a running terminal session.",
 76 |         inputSchema: zodToJsonSchema(ForceTerminateArgsSchema),
 77 |       },
 78 |       {
 79 |         name: "list_sessions",
 80 |         description:
 81 |           "List all active terminal sessions.",
 82 |         inputSchema: zodToJsonSchema(ListSessionsArgsSchema),
 83 |       },
 84 |       {
 85 |         name: "list_processes",
 86 |         description:
 87 |           "List all running processes. Returns process information including PID, " +
 88 |           "command name, CPU usage, and memory usage.",
 89 |         inputSchema: {
 90 |           type: "object",
 91 |           properties: {},
 92 |           required: [],
 93 |         },
 94 |       },
 95 |       {
 96 |         name: "kill_process",
 97 |         description:
 98 |           "Terminate a running process by PID. Use with caution as this will " +
 99 |           "forcefully terminate the specified process.",
100 |         inputSchema: zodToJsonSchema(KillProcessArgsSchema),
101 |       },
102 |       {
103 |         name: "block_command",
104 |         description:
105 |           "Add a command to the blacklist. Once blocked, the command cannot be executed until unblocked.",
106 |         inputSchema: zodToJsonSchema(BlockCommandArgsSchema),
107 |       },
108 |       {
109 |         name: "unblock_command",
110 |         description:
111 |           "Remove a command from the blacklist. Once unblocked, the command can be executed normally.",
112 |         inputSchema: zodToJsonSchema(UnblockCommandArgsSchema),
113 |       },
114 |       {
115 |         name: "list_blocked_commands",
116 |         description:
117 |           "List all currently blocked commands.",
118 |         inputSchema: {
119 |           type: "object",
120 |           properties: {},
121 |           required: [],
122 |         },
123 |       },
124 |       // Filesystem tools
125 |       {
126 |         name: "read_file",
127 |         description:
128 |           "Read the complete contents of a file from the file system. " +
129 |           "Handles various text encodings and provides detailed error messages " +
130 |           "if the file cannot be read. Only works within allowed directories.",
131 |         inputSchema: zodToJsonSchema(ReadFileArgsSchema),
132 |       },
133 |       {
134 |         name: "read_multiple_files",
135 |         description:
136 |           "Read the contents of multiple files simultaneously. " +
137 |           "Each file's content is returned with its path as a reference. " +
138 |           "Failed reads for individual files won't stop the entire operation. " +
139 |           "Only works within allowed directories.",
140 |         inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema),
141 |       },
142 |       {
143 |         name: "write_file",
144 |         description:
145 |           "Completely replace file contents. Best for large changes (>20% of file) or when edit_block fails. " +
146 |           "Use with caution as it will overwrite existing files. Only works within allowed directories.",
147 |         inputSchema: zodToJsonSchema(WriteFileArgsSchema),
148 |       },
149 |       {
150 |         name: "create_directory",
151 |         description:
152 |           "Create a new directory or ensure a directory exists. Can create multiple " +
153 |           "nested directories in one operation. Only works within allowed directories.",
154 |         inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema),
155 |       },
156 |       {
157 |         name: "list_directory",
158 |         description:
159 |           "Get a detailed listing of all files and directories in a specified path. " +
160 |           "Results distinguish between files and directories with [FILE] and [DIR] prefixes. " +
161 |           "Only works within allowed directories.",
162 |         inputSchema: zodToJsonSchema(ListDirectoryArgsSchema),
163 |       },
164 |       {
165 |         name: "move_file",
166 |         description:
167 |           "Move or rename files and directories. Can move files between directories " +
168 |           "and rename them in a single operation. Both source and destination must be " +
169 |           "within allowed directories.",
170 |         inputSchema: zodToJsonSchema(MoveFileArgsSchema),
171 |       },
172 |       {
173 |         name: "search_files",
174 |         description:
175 |           "Recursively search for files and directories matching a pattern. " +
176 |           "Searches through all subdirectories from the starting path. " +
177 |           "Only searches within allowed directories.",
178 |         inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
179 |       },
180 |       {
181 |         name: "get_file_info",
182 |         description:
183 |           "Retrieve detailed metadata about a file or directory including size, " +
184 |           "creation time, last modified time, permissions, and type. " +
185 |           "Only works within allowed directories.",
186 |         inputSchema: zodToJsonSchema(GetFileInfoArgsSchema),
187 |       },
188 |       {
189 |         name: "list_allowed_directories",
190 |         description: 
191 |           "Returns the list of directories that this server is allowed to access.",
192 |         inputSchema: {
193 |           type: "object",
194 |           properties: {},
195 |           required: [],
196 |         },
197 |       },
198 |       {
199 |         name: "edit_block",
200 |         description:
201 |             "Apply surgical text replacements to files. Best for small changes (<20% of file size). " +
202 |             "Multiple blocks can be used for separate changes. Will verify changes after application. " +
203 |             "Format: filepath, then <<<<<<< SEARCH, content to find, =======, new content, >>>>>>> REPLACE.",
204 |         inputSchema: zodToJsonSchema(EditBlockArgsSchema),
205 |       },
206 |     ],
207 |   };
208 | });
209 | 
210 | server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
211 |   try {
212 |     const { name, arguments: args } = request.params;
213 | 
214 |     switch (name) {
215 |       // Terminal tools
216 |       case "execute_command": {
217 |         const parsed = ExecuteCommandArgsSchema.parse(args);
218 |         return executeCommand(parsed);
219 |       }
220 |       case "read_output": {
221 |         const parsed = ReadOutputArgsSchema.parse(args);
222 |         return readOutput(parsed);
223 |       }
224 |       case "force_terminate": {
225 |         const parsed = ForceTerminateArgsSchema.parse(args);
226 |         return forceTerminate(parsed);
227 |       }
228 |       case "list_sessions":
229 |         return listSessions();
230 |       case "list_processes":
231 |         return listProcesses();
232 |       case "kill_process": {
233 |         const parsed = KillProcessArgsSchema.parse(args);
234 |         return killProcess(parsed);
235 |       }
236 |       case "block_command": {
237 |         const parsed = BlockCommandArgsSchema.parse(args);
238 |         const blockResult = await commandManager.blockCommand(parsed.command);
239 |         return {
240 |           content: [{ type: "text", text: blockResult }],
241 |         };
242 |       }
243 |       case "unblock_command": {
244 |         const parsed = UnblockCommandArgsSchema.parse(args);
245 |         const unblockResult = await commandManager.unblockCommand(parsed.command);
246 |         return {
247 |           content: [{ type: "text", text: unblockResult }],
248 |         };
249 |       }
250 |       case "list_blocked_commands": {
251 |         const blockedCommands = await commandManager.listBlockedCommands();
252 |         return {
253 |           content: [{ type: "text", text: blockedCommands.join('\n') }],
254 |         };
255 |       }
256 |       
257 |       // Filesystem tools
258 |       case "edit_block": {
259 |         const parsed = EditBlockArgsSchema.parse(args);
260 |         const { filePath, searchReplace } = await parseEditBlock(parsed.blockContent);
261 |         await performSearchReplace(filePath, searchReplace);
262 |         return {
263 |           content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
264 |         };
265 |       }
266 |       case "read_file": {
267 |         const parsed = ReadFileArgsSchema.parse(args);
268 |         const content = await readFile(parsed.path);
269 |         return {
270 |           content: [{ type: "text", text: content }],
271 |         };
272 |       }
273 |       case "read_multiple_files": {
274 |         const parsed = ReadMultipleFilesArgsSchema.parse(args);
275 |         const results = await readMultipleFiles(parsed.paths);
276 |         return {
277 |           content: [{ type: "text", text: results.join("\n---\n") }],
278 |         };
279 |       }
280 |       case "write_file": {
281 |         const parsed = WriteFileArgsSchema.parse(args);
282 |         await writeFile(parsed.path, parsed.content);
283 |         return {
284 |           content: [{ type: "text", text: `Successfully wrote to ${parsed.path}` }],
285 |         };
286 |       }
287 |       case "create_directory": {
288 |         const parsed = CreateDirectoryArgsSchema.parse(args);
289 |         await createDirectory(parsed.path);
290 |         return {
291 |           content: [{ type: "text", text: `Successfully created directory ${parsed.path}` }],
292 |         };
293 |       }
294 |       case "list_directory": {
295 |         const parsed = ListDirectoryArgsSchema.parse(args);
296 |         const entries = await listDirectory(parsed.path);
297 |         return {
298 |           content: [{ type: "text", text: entries.join('\n') }],
299 |         };
300 |       }
301 |       case "move_file": {
302 |         const parsed = MoveFileArgsSchema.parse(args);
303 |         await moveFile(parsed.source, parsed.destination);
304 |         return {
305 |           content: [{ type: "text", text: `Successfully moved ${parsed.source} to ${parsed.destination}` }],
306 |         };
307 |       }
308 |       case "search_files": {
309 |         const parsed = SearchFilesArgsSchema.parse(args);
310 |         const results = await searchFiles(parsed.path, parsed.pattern);
311 |         return {
312 |           content: [{ type: "text", text: results.length > 0 ? results.join('\n') : "No matches found" }],
313 |         };
314 |       }
315 |       case "get_file_info": {
316 |         const parsed = GetFileInfoArgsSchema.parse(args);
317 |         const info = await getFileInfo(parsed.path);
318 |         return {
319 |           content: [{ 
320 |             type: "text", 
321 |             text: Object.entries(info)
322 |               .map(([key, value]) => `${key}: ${value}`)
323 |               .join('\n') 
324 |           }],
325 |         };
326 |       }
327 |       case "list_allowed_directories": {
328 |         const directories = listAllowedDirectories();
329 |         return {
330 |           content: [{ 
331 |             type: "text", 
332 |             text: `Allowed directories:\n${directories.join('\n')}` 
333 |           }],
334 |         };
335 |       }
336 |       default:
337 |         throw new Error(`Unknown tool: ${name}`);
338 |     }
339 |   } catch (error) {
340 |     const errorMessage = error instanceof Error ? error.message : String(error);
341 |     return {
342 |       content: [{ type: "text", text: `Error: ${errorMessage}` }],
343 |       isError: true,
344 |     };
345 |   }
346 | });
```