# 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 | 
3 |
4 | [](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
5 | [](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
6 | [](https://www.buymeacoffee.com/wonderwhyer)
7 | [](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 | [ 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 | [
191 | https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg
192 | )
193 |
194 | [
196 | https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg)
197 |
198 | [
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 | [
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 | });
```