# 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:
--------------------------------------------------------------------------------
```
.git
.gitignore
.DS_Store
.history
.idea
src/
tsconfig.json
*.log
work/
config.json
setup-claude-server.js
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# TypeScript build output
dist/
*.tsbuildinfo
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Environment variables
.env
.env.local
.env.*.local
# Logs
logs/
*.log
# Coverage directory used by tools like istanbul
coverage/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
.idea
.history
server.log
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Desktop Commander MCP

[](https://www.npmjs.com/package/@wonderwhy-er/desktop-commander)
[](https://smithery.ai/server/@wonderwhy-er/desktop-commander)
[](https://www.buymeacoffee.com/wonderwhyer)
[](https://discord.gg/7cbccwRp)
Short version. Two key things. Terminal commands and diff based file editing.
<a href="https://glama.ai/mcp/servers/zempur9oh4">
<img width="380" height="200" src="https://glama.ai/mcp/servers/zempur9oh4/badge" alt="Claude Desktop Commander MCP server" />
</a>
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Handling Long-Running Commands](#handling-long-running-commands)
- [Work in Progress and TODOs](#work-in-progress-and-todos)
- [Media links](#media)
- [Testimonials](#testimonials)
- [Contributing](#contributing)
- [License](#license)
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 .
## Features
- Execute terminal commands with output streaming
- Command timeout and background execution support
- Process management (list and kill processes)
- Session management for long-running commands
- Full filesystem operations:
- Read/write files
- Create/list directories
- Move files/directories
- Search files
- Get file metadata
- Code editing capabilities:
- Surgical text replacements for small changes
- Full file rewrites for major changes
- Multiple file support
- Pattern-based replacements
## Installation
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).
### Option 1: Installing via Smithery
To install Desktop Commander for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@wonderwhy-er/desktop-commander):
```bash
npx -y @smithery/cli install @wonderwhy-er/desktop-commander --client claude
```
### Option 2: Install trough npx
Just run this in terminal
```
npx @wonderwhy-er/desktop-commander setup
```
Restart Claude if running
### Option 3: Add to claude_desktop_config by hand
Add this entry to your claude_desktop_config.json (on Mac, found at ~/Library/Application\ Support/Claude/claude_desktop_config.json):
```json
{
"mcpServers": {
"desktop-commander": {
"command": "npx",
"args": [
"-y",
"@wonderwhy-er/desktop-commander"
]
}
}
}
```
Restart Claude if running
### Option 4: Checkout locally
1. Clone and build:
```bash
git clone https://github.com/wonderwhy-er/ClaudeComputerCommander.git
cd ClaudeComputerCommander
npm run setup
```
Restart Claude if running
The setup command will:
- Install dependencies
- Build the server
- Configure Claude's desktop app
- Add MCP servers to Claude's config if needed
## Usage
The server provides these tool categories:
### Terminal Tools
- `execute_command`: Run commands with configurable timeout
- `read_output`: Get output from long-running commands
- `force_terminate`: Stop running command sessions
- `list_sessions`: View active command sessions
- `list_processes`: View system processes
- `kill_process`: Terminate processes by PID
- `block_command`/`unblock_command`: Manage command blacklist
### Filesystem Tools
- `read_file`/`write_file`: File operations
- `create_directory`/`list_directory`: Directory management
- `move_file`: Move/rename files
- `search_files`: Pattern-based file search
- `get_file_info`: File metadata
### Edit Tools
- `edit_block`: Apply surgical text replacements (best for changes <20% of file size)
- `write_file`: Complete file rewrites (best for large changes >20% or when edit_block fails)
Search/Replace Block Format:
```
filepath.ext
<<<<<<< SEARCH
existing code to replace
=======
new code to insert
>>>>>>> REPLACE
```
Example:
```
src/main.js
<<<<<<< SEARCH
console.log("old message");
=======
console.log("new message");
>>>>>>> REPLACE
```
## Handling Long-Running Commands
For commands that may take a while:
1. `execute_command` returns after timeout with initial output
2. Command continues in background
3. Use `read_output` with PID to get new output
4. Use `force_terminate` to stop if needed
## Model Context Protocol Integration
This project extends the MCP Filesystem Server to enable:
- Local server support in Claude Desktop
- Full system command execution
- Process management
- File operations
- Code editing with search/replace blocks
Created as part of exploring Claude MCPs: https://youtube.com/live/TlbjFDbl5Us
## Work in Progress and TODOs
The following features are currently being developed or planned:
- **Better code search** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/17)) - Enhanced code exploration with context-aware results
- **Better configurations** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/16)) - Improved settings for allowed paths, commands and shell environment
- **Windows environment fixes** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/13)) - Resolving issues specific to Windows platforms
- **Linux improvements** ([in progress](https://github.com/wonderwhy-er/ClaudeDesktopCommander/pull/12)) - Enhancing compatibility with various Linux distributions
- **Support for WSL** - Windows Subsystem for Linux integration
- **Support for SSH** - Remote server command execution
- **Installation troubleshooting guide** - Comprehensive help for setup issues
## Media
Learn more about this project through these resources:
### Article
[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.
### Video
[Claude Desktop Commander Video Tutorial](https://www.youtube.com/watch?v=ly3bed99Dy8) - Watch how to set up and use the Commander effectively.
### Community
Join our [Discord server](https://discord.gg/7cbccwRp) to get help, share feedback, and connect with other users.
## Testimonials
[ https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyyBt6_ShdDX_rIOad4AaABAg
](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyyBt6_ShdDX_rIOad4AaABAg
)
[
https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgztdHvDMqTb9jiqnf54AaABAg
)
[
https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=UgyQFTmYLJ4VBwIlmql4AaABAg)
[
https://www.youtube.com/watch?v=ly3bed99Dy8&lc=Ugy4-exy166_Ma7TH-h4AaABAg](https://www.youtube.com/watch?v=ly3bed99Dy8&lc=Ugy4-exy166_Ma7TH-h4AaABAg)
[
https://medium.com/@pharmx/you-sir-are-my-hero-62cff5836a3e](https://medium.com/@pharmx/you-sir-are-my-hero-62cff5836a3e)
## Contributing
If you find this project useful, please consider giving it a ⭐ star on GitHub! This helps others discover the project and encourages further development.
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:
- **Found a bug?** Open an issue at [github.com/wonderwhy-er/ClaudeComputerCommander/issues](https://github.com/wonderwhy-er/ClaudeComputerCommander/issues)
- **Have a feature idea?** Submit a feature request in the issues section
- **Want to contribute code?** Fork the repository, create a branch, and submit a pull request
- **Questions or discussions?** Start a discussion in the GitHub Discussions tab
All contributions, big or small, are greatly appreciated!
If you find this tool valuable for your workflow, please consider [supporting the project](https://www.buymeacoffee.com/wonderwhyer).
## License
MIT
```
--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------
```typescript
export const VERSION = '0.1.19';
```
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
```json
{
"blockedCommands": [
"format",
"mount",
"umount",
"mkfs",
"fdisk",
"dd",
"sudo",
"su",
"passwd",
"adduser",
"useradd",
"usermod",
"groupadd"
]
}
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
import path from 'path';
import process from 'process';
export const CONFIG_FILE = path.join(process.cwd(), 'config.json');
export const LOG_FILE = path.join(process.cwd(), 'server.log');
export const ERROR_LOG_FILE = path.join(process.cwd(), 'error.log');
export const DEFAULT_COMMAND_TIMEOUT = 1000; // milliseconds
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
properties: {}
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({ command: 'node', args: ['dist/index.js'] })
exampleConfig: {}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "Node16",
"moduleResolution": "node16",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"skipLibCheck": true,
"diagnostics": true,
"extendedDiagnostics": true,
"listEmittedFiles": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine
# Create app directory
WORKDIR /usr/src/app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies without triggering any unwanted scripts
RUN npm install --ignore-scripts
# Copy all source code
COPY . .
# Build the application
RUN npm run build
# Expose port if needed (not specified, so using none)
# Command to run the server
CMD [ "node", "dist/index.js" ]
```
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
```typescript
import { ChildProcess } from 'child_process';
export interface ProcessInfo {
pid: number;
command: string;
cpu: string;
memory: string;
}
export interface TerminalSession {
pid: number;
process: ChildProcess;
lastOutput: string;
isBlocked: boolean;
startTime: Date;
}
export interface CommandExecutionResult {
pid: number;
output: string;
isBlocked: boolean;
}
export interface ActiveSession {
pid: number;
isBlocked: boolean;
runtime: number;
}
export interface CompletedSession {
pid: number;
output: string;
exitCode: number | null;
startTime: Date;
endTime: Date;
}
```
--------------------------------------------------------------------------------
/scripts/sync-version.js:
--------------------------------------------------------------------------------
```javascript
import { readFileSync, writeFileSync } from 'fs';
import path from 'path';
function bumpVersion(version, type = 'patch') {
const [major, minor, patch] = version.split('.').map(Number);
switch(type) {
case 'major':
return `${major + 1}.0.0`;
case 'minor':
return `${major}.${minor + 1}.0`;
case 'patch':
default:
return `${major}.${minor}.${patch + 1}`;
}
}
// Read command line arguments
const shouldBump = process.argv.includes('--bump');
const bumpType = process.argv.includes('--major') ? 'major'
: process.argv.includes('--minor') ? 'minor'
: 'patch';
// Read version from package.json
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
let version = pkg.version;
// Bump version if requested
if (shouldBump) {
version = bumpVersion(version, bumpType);
// Update package.json
pkg.version = version;
writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
}
// Update version.ts
const versionFileContent = `export const VERSION = '${version}';\n`;
writeFileSync('src/version.ts', versionFileContent);
console.log(`Version ${version} synchronized${shouldBump ? ' and bumped' : ''}`);
```
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
```javascript
import { parseEditBlock, performSearchReplace } from '../dist/tools/edit.js';
async function runTests() {
try {
// Test parseEditBlock
const testBlock = `test.txt
<<<<<<< SEARCH
old content
=======
new content
>>>>>>> REPLACE`;
const parsed = await parseEditBlock(testBlock);
console.log('Parse test passed:', parsed);
// Create a test file
const fs = await import('fs/promises');
const testFilePath = 'test/test.txt';
await fs.writeFile(testFilePath, 'This is old content to replace');
// Test performSearchReplace
await performSearchReplace(testFilePath, {
search: 'old content',
replace: 'new content'
});
const result = await fs.readFile(testFilePath, 'utf8');
console.log('File content after replacement:', result);
if (result.includes('new content')) {
console.log('Replace test passed!');
} else {
throw new Error('Replace test failed!');
}
// Cleanup
await fs.unlink(testFilePath);
console.log('All tests passed! 🎉');
} catch (error) {
console.error('Test failed:', error);
process.exit(1);
}
}
runTests();
```
--------------------------------------------------------------------------------
/src/tools/schemas.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
// Terminal tools schemas
export const ExecuteCommandArgsSchema = z.object({
command: z.string(),
timeout_ms: z.number().optional(),
});
export const ReadOutputArgsSchema = z.object({
pid: z.number(),
});
export const ForceTerminateArgsSchema = z.object({
pid: z.number(),
});
export const ListSessionsArgsSchema = z.object({});
export const KillProcessArgsSchema = z.object({
pid: z.number(),
});
export const BlockCommandArgsSchema = z.object({
command: z.string(),
});
export const UnblockCommandArgsSchema = z.object({
command: z.string(),
});
// Filesystem tools schemas
export const ReadFileArgsSchema = z.object({
path: z.string(),
});
export const ReadMultipleFilesArgsSchema = z.object({
paths: z.array(z.string()),
});
export const WriteFileArgsSchema = z.object({
path: z.string(),
content: z.string(),
});
export const CreateDirectoryArgsSchema = z.object({
path: z.string(),
});
export const ListDirectoryArgsSchema = z.object({
path: z.string(),
});
export const MoveFileArgsSchema = z.object({
source: z.string(),
destination: z.string(),
});
export const SearchFilesArgsSchema = z.object({
path: z.string(),
pattern: z.string(),
});
export const GetFileInfoArgsSchema = z.object({
path: z.string(),
});
// Edit tools schemas
export const EditBlockArgsSchema = z.object({
blockContent: z.string(),
});
```
--------------------------------------------------------------------------------
/src/tools/edit.ts:
--------------------------------------------------------------------------------
```typescript
import { readFile, writeFile } from './filesystem.js';
interface SearchReplace {
search: string;
replace: string;
}
export async function performSearchReplace(filePath: string, block: SearchReplace): Promise<void> {
const content = await readFile(filePath);
// Find first occurrence
const searchIndex = content.indexOf(block.search);
if (searchIndex === -1) {
throw new Error(`Search content not found in ${filePath}`);
}
// Replace content
const newContent =
content.substring(0, searchIndex) +
block.replace +
content.substring(searchIndex + block.search.length);
await writeFile(filePath, newContent);
}
export async function parseEditBlock(blockContent: string): Promise<{
filePath: string;
searchReplace: SearchReplace;
}> {
const lines = blockContent.split('\n');
// First line should be the file path
const filePath = lines[0].trim();
// Find the markers
const searchStart = lines.indexOf('<<<<<<< SEARCH');
const divider = lines.indexOf('=======');
const replaceEnd = lines.indexOf('>>>>>>> REPLACE');
if (searchStart === -1 || divider === -1 || replaceEnd === -1) {
throw new Error('Invalid edit block format - missing markers');
}
// Extract search and replace content
const search = lines.slice(searchStart + 1, divider).join('\n');
const replace = lines.slice(divider + 1, replaceEnd).join('\n');
return {
filePath,
searchReplace: { search, replace }
};
}
```
--------------------------------------------------------------------------------
/src/tools/process.ts:
--------------------------------------------------------------------------------
```typescript
import { exec } from 'child_process';
import { promisify } from 'util';
import os from 'os';
import { ProcessInfo } from '../types.js';
import { KillProcessArgsSchema } from './schemas.js';
const execAsync = promisify(exec);
export async function listProcesses(): Promise<{content: Array<{type: string, text: string}>}> {
const command = os.platform() === 'win32' ? 'tasklist' : 'ps aux';
try {
const { stdout } = await execAsync(command);
const processes = stdout.split('\n')
.slice(1)
.filter(Boolean)
.map(line => {
const parts = line.split(/\s+/);
return {
pid: parseInt(parts[1]),
command: parts[parts.length - 1],
cpu: parts[2],
memory: parts[3],
} as ProcessInfo;
});
return {
content: [{
type: "text",
text: processes.map(p =>
`PID: ${p.pid}, Command: ${p.command}, CPU: ${p.cpu}, Memory: ${p.memory}`
).join('\n')
}],
};
} catch (error) {
throw new Error('Failed to list processes');
}
}
export async function killProcess(args: unknown) {
const parsed = KillProcessArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for kill_process: ${parsed.error}`);
}
try {
process.kill(parsed.data.pid);
return {
content: [{ type: "text", text: `Successfully terminated process ${parsed.data.pid}` }],
};
} catch (error) {
throw new Error(`Failed to kill process: ${error instanceof Error ? error.message : String(error)}`);
}
}
```
--------------------------------------------------------------------------------
/src/command-manager.ts:
--------------------------------------------------------------------------------
```typescript
import fs from 'fs/promises';
import { CONFIG_FILE } from './config.js';
class CommandManager {
private blockedCommands: Set<string> = new Set();
async loadBlockedCommands(): Promise<void> {
try {
const configData = await fs.readFile(CONFIG_FILE, 'utf-8');
const config = JSON.parse(configData);
this.blockedCommands = new Set(config.blockedCommands);
} catch (error) {
this.blockedCommands = new Set();
}
}
async saveBlockedCommands(): Promise<void> {
try {
const config = {
blockedCommands: Array.from(this.blockedCommands)
};
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
} catch (error) {
// Handle error if needed
}
}
validateCommand(command: string): boolean {
const baseCommand = command.split(' ')[0].toLowerCase().trim();
return !this.blockedCommands.has(baseCommand);
}
async blockCommand(command: string): Promise<boolean> {
command = command.toLowerCase().trim();
if (this.blockedCommands.has(command)) {
return false;
}
this.blockedCommands.add(command);
await this.saveBlockedCommands();
return true;
}
async unblockCommand(command: string): Promise<boolean> {
command = command.toLowerCase().trim();
if (!this.blockedCommands.has(command)) {
return false;
}
this.blockedCommands.delete(command);
await this.saveBlockedCommands();
return true;
}
listBlockedCommands(): string[] {
return Array.from(this.blockedCommands).sort();
}
}
export const commandManager = new CommandManager();
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@wonderwhy-er/desktop-commander",
"version": "0.1.19",
"description": "MCP server for terminal operations and file editing",
"license": "MIT",
"author": "Eduards Ruzga",
"homepage": "https://github.com/wonderwhy-er/ClaudeComputerCommander",
"bugs": "https://github.com/wonderwhy-er/ClaudeComputerCommander/issues",
"type": "module",
"bin": {
"desktop-commander": "dist/index.js",
"setup": "dist/setup-claude-server.js"
},
"files": [
"dist"
],
"scripts": {
"sync-version": "node scripts/sync-version.js",
"bump": "node scripts/sync-version.js --bump",
"bump:minor": "node scripts/sync-version.js --bump --minor",
"bump:major": "node scripts/sync-version.js --bump --major",
"build": "tsc && shx cp setup-claude-server.js dist/ && shx chmod +x dist/*.js",
"watch": "tsc --watch",
"start": "node dist/index.js",
"setup": "npm install && npm run build && node setup-claude-server.js",
"prepare": "npm run build",
"test": "node test/test.js",
"test:watch": "nodemon test/test.js",
"link:local": "npm run build && npm link",
"unlink:local": "npm unlink",
"inspector": "npx @modelcontextprotocol/inspector dist/index.js"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"mcp",
"model-context-protocol",
"terminal",
"claude",
"ai",
"command-line",
"process-management",
"file-editing",
"code-editing",
"diff",
"patch",
"block-editing",
"file-system",
"text-manipulation",
"code-modification",
"surgical-edits",
"file-operations"
],
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1",
"glob": "^10.3.10",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
"@types/node": "^20.11.0",
"nodemon": "^3.0.2",
"shx": "^0.3.4",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { server } from './server.js';
import { commandManager } from './command-manager.js';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
async function runSetup() {
const setupScript = join(__dirname, 'setup-claude-server.js');
const { default: setupModule } = await import(setupScript);
if (typeof setupModule === 'function') {
await setupModule();
}
}
async function runServer() {
try {
// Check if first argument is "setup"
if (process.argv[2] === 'setup') {
await runSetup();
return;
}
// Handle uncaught exceptions
process.on('uncaughtException', async (error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
process.exit(1);
});
// Handle unhandled rejections
process.on('unhandledRejection', async (reason) => {
const errorMessage = reason instanceof Error ? reason.message : String(reason);
process.exit(1);
});
const transport = new StdioServerTransport();
// Load blocked commands from config file
await commandManager.loadBlockedCommands();
await server.connect(transport);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(JSON.stringify({
type: 'error',
timestamp: new Date().toISOString(),
message: `Failed to start server: ${errorMessage}`
}) + '\n');
process.exit(1);
}
}
runServer().catch(async (error) => {
const errorMessage = error instanceof Error ? error.message : String(error);
process.stderr.write(JSON.stringify({
type: 'error',
timestamp: new Date().toISOString(),
message: `Fatal error running server: ${errorMessage}`
}) + '\n');
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/tools/execute.ts:
--------------------------------------------------------------------------------
```typescript
import { terminalManager } from '../terminal-manager.js';
import { commandManager } from '../command-manager.js';
import { ExecuteCommandArgsSchema, ReadOutputArgsSchema, ForceTerminateArgsSchema, ListSessionsArgsSchema } from './schemas.js';
export async function executeCommand(args: unknown) {
const parsed = ExecuteCommandArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for execute_command: ${parsed.error}`);
}
if (!commandManager.validateCommand(parsed.data.command)) {
throw new Error(`Command not allowed: ${parsed.data.command}`);
}
const result = await terminalManager.executeCommand(
parsed.data.command,
parsed.data.timeout_ms
);
return {
content: [{
type: "text",
text: `Command started with PID ${result.pid}\nInitial output:\n${result.output}${
result.isBlocked ? '\nCommand is still running. Use read_output to get more output.' : ''
}`
}],
};
}
export async function readOutput(args: unknown) {
const parsed = ReadOutputArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for read_output: ${parsed.error}`);
}
const output = terminalManager.getNewOutput(parsed.data.pid);
return {
content: [{
type: "text",
text: output === null
? `No session found for PID ${parsed.data.pid}`
: output || 'No new output available'
}],
};
}
export async function forceTerminate(args: unknown) {
const parsed = ForceTerminateArgsSchema.safeParse(args);
if (!parsed.success) {
throw new Error(`Invalid arguments for force_terminate: ${parsed.error}`);
}
const success = terminalManager.forceTerminate(parsed.data.pid);
return {
content: [{
type: "text",
text: success
? `Successfully initiated termination of session ${parsed.data.pid}`
: `No active session found for PID ${parsed.data.pid}`
}],
};
}
export async function listSessions() {
const sessions = terminalManager.listActiveSessions();
return {
content: [{
type: "text",
text: sessions.length === 0
? 'No active sessions'
: sessions.map(s =>
`PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s`
).join('\n')
}],
};
}
```
--------------------------------------------------------------------------------
/setup-claude-server.js:
--------------------------------------------------------------------------------
```javascript
import { homedir, platform } from 'os';
import { join } from 'path';
import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Determine OS and set appropriate config path and command
const isWindows = platform() === 'win32';
const claudeConfigPath = isWindows
? join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json')
: join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
// Setup logging
const LOG_FILE = join(__dirname, 'setup.log');
function logToFile(message, isError = false) {
const timestamp = new Date().toISOString();
const logMessage = `${timestamp} - ${isError ? 'ERROR: ' : ''}${message}\n`;
try {
appendFileSync(LOG_FILE, logMessage);
// For setup script, we'll still output to console but in JSON format
const jsonOutput = {
type: isError ? 'error' : 'info',
timestamp,
message
};
process.stdout.write(JSON.stringify(jsonOutput) + '\n');
} catch (err) {
// Last resort error handling
process.stderr.write(JSON.stringify({
type: 'error',
timestamp: new Date().toISOString(),
message: `Failed to write to log file: ${err.message}`
}) + '\n');
}
}
// Check if config file exists and create default if not
if (!existsSync(claudeConfigPath)) {
logToFile(`Claude config file not found at: ${claudeConfigPath}`);
logToFile('Creating default config file...');
// Create the directory if it doesn't exist
const configDir = dirname(claudeConfigPath);
if (!existsSync(configDir)) {
import('fs').then(fs => fs.mkdirSync(configDir, { recursive: true }));
}
// Create default config
const defaultConfig = {
"serverConfig": isWindows
? {
"command": "cmd.exe",
"args": ["/c"]
}
: {
"command": "/bin/sh",
"args": ["-c"]
}
};
writeFileSync(claudeConfigPath, JSON.stringify(defaultConfig, null, 2));
logToFile('Default config file created. Please update it with your Claude API credentials.');
}
try {
// Read existing config
const configData = readFileSync(claudeConfigPath, 'utf8');
const config = JSON.parse(configData);
// Prepare the new server config based on OS
// Determine if running through npx or locally
const isNpx = import.meta.url.endsWith('dist/setup-claude-server.js');
const serverConfig = isNpx ? {
"command": "npx",
"args": [
"@wonderwhy-er/desktop-commander"
]
} : {
"command": "node",
"args": [
join(__dirname, 'dist', 'index.js')
]
};
// Add or update the terminal server config
if (!config.mcpServers) {
config.mcpServers = {};
}
config.mcpServers.desktopCommander = serverConfig;
// Add puppeteer server if not present
/*if (!config.mcpServers.puppeteer) {
config.mcpServers.puppeteer = {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
};
}*/
// Write the updated config back
writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), 'utf8');
logToFile('Successfully added MCP servers to Claude configuration!');
logToFile(`Configuration location: ${claudeConfigPath}`);
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');
} catch (error) {
logToFile(`Error updating Claude configuration: ${error}`, true);
process.exit(1);
}
```
--------------------------------------------------------------------------------
/src/terminal-manager.ts:
--------------------------------------------------------------------------------
```typescript
import { spawn } from 'child_process';
import { TerminalSession, CommandExecutionResult, ActiveSession } from './types.js';
import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
interface CompletedSession {
pid: number;
output: string;
exitCode: number | null;
startTime: Date;
endTime: Date;
}
export class TerminalManager {
private sessions: Map<number, TerminalSession> = new Map();
private completedSessions: Map<number, CompletedSession> = new Map();
async executeCommand(command: string, timeoutMs: number = DEFAULT_COMMAND_TIMEOUT): Promise<CommandExecutionResult> {
const process = spawn(command, [], { shell: true });
let output = '';
// Ensure process.pid is defined before proceeding
if (!process.pid) {
throw new Error('Failed to get process ID');
}
const session: TerminalSession = {
pid: process.pid,
process,
lastOutput: '',
isBlocked: false,
startTime: new Date()
};
this.sessions.set(process.pid, session);
return new Promise((resolve) => {
process.stdout.on('data', (data) => {
const text = data.toString();
output += text;
session.lastOutput += text;
});
process.stderr.on('data', (data) => {
const text = data.toString();
output += text;
session.lastOutput += text;
});
setTimeout(() => {
session.isBlocked = true;
resolve({
pid: process.pid!,
output,
isBlocked: true
});
}, timeoutMs);
process.on('exit', (code) => {
if (process.pid) {
// Store completed session before removing active session
this.completedSessions.set(process.pid, {
pid: process.pid,
output: output + session.lastOutput, // Combine all output
exitCode: code,
startTime: session.startTime,
endTime: new Date()
});
// Keep only last 100 completed sessions
if (this.completedSessions.size > 100) {
const oldestKey = Array.from(this.completedSessions.keys())[0];
this.completedSessions.delete(oldestKey);
}
this.sessions.delete(process.pid);
}
resolve({
pid: process.pid!,
output,
isBlocked: false
});
});
});
}
getNewOutput(pid: number): string | null {
// First check active sessions
const session = this.sessions.get(pid);
if (session) {
const output = session.lastOutput;
session.lastOutput = '';
return output;
}
// Then check completed sessions
const completedSession = this.completedSessions.get(pid);
if (completedSession) {
// Format completion message with exit code and runtime
const runtime = (completedSession.endTime.getTime() - completedSession.startTime.getTime()) / 1000;
return `Process completed with exit code ${completedSession.exitCode}\nRuntime: ${runtime}s\nFinal output:\n${completedSession.output}`;
}
return null;
}
forceTerminate(pid: number): boolean {
const session = this.sessions.get(pid);
if (!session) {
return false;
}
try {
session.process.kill('SIGINT');
setTimeout(() => {
if (this.sessions.has(pid)) {
session.process.kill('SIGKILL');
}
}, 1000);
return true;
} catch (error) {
console.error(`Failed to terminate process ${pid}:`, error);
return false;
}
}
listActiveSessions(): ActiveSession[] {
const now = new Date();
return Array.from(this.sessions.values()).map(session => ({
pid: session.pid,
isBlocked: session.isBlocked,
runtime: now.getTime() - session.startTime.getTime()
}));
}
listCompletedSessions(): CompletedSession[] {
return Array.from(this.completedSessions.values());
}
}
export const terminalManager = new TerminalManager();
```
--------------------------------------------------------------------------------
/src/tools/filesystem.ts:
--------------------------------------------------------------------------------
```typescript
import fs from "fs/promises";
import path from "path";
import os from 'os';
// Store allowed directories
const allowedDirectories: string[] = [
process.cwd(), // Current working directory
os.homedir() // User's home directory
];
// Normalize all paths consistently
function normalizePath(p: string): string {
return path.normalize(p).toLowerCase();
}
function expandHome(filepath: string): string {
if (filepath.startsWith('~/') || filepath === '~') {
return path.join(os.homedir(), filepath.slice(1));
}
return filepath;
}
// Security utilities
export async function validatePath(requestedPath: string): Promise<string> {
const expandedPath = expandHome(requestedPath);
const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
: path.resolve(process.cwd(), expandedPath);
const normalizedRequested = normalizePath(absolute);
// Check if path is within allowed directories
const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(normalizePath(dir)));
if (!isAllowed) {
throw new Error(`Access denied - path outside allowed directories: ${absolute}`);
}
// Handle symlinks by checking their real path
try {
const realPath = await fs.realpath(absolute);
const normalizedReal = normalizePath(realPath);
const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(normalizePath(dir)));
if (!isRealPathAllowed) {
throw new Error("Access denied - symlink target outside allowed directories");
}
return realPath;
} catch (error) {
// For new files that don't exist yet, verify parent directory
const parentDir = path.dirname(absolute);
try {
const realParentPath = await fs.realpath(parentDir);
const normalizedParent = normalizePath(realParentPath);
const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(normalizePath(dir)));
if (!isParentAllowed) {
throw new Error("Access denied - parent directory outside allowed directories");
}
return absolute;
} catch {
throw new Error(`Parent directory does not exist: ${parentDir}`);
}
}
}
// File operation tools
export async function readFile(filePath: string): Promise<string> {
const validPath = await validatePath(filePath);
return fs.readFile(validPath, "utf-8");
}
export async function writeFile(filePath: string, content: string): Promise<void> {
const validPath = await validatePath(filePath);
await fs.writeFile(validPath, content, "utf-8");
}
export async function readMultipleFiles(paths: string[]): Promise<string[]> {
return Promise.all(
paths.map(async (filePath: string) => {
try {
const validPath = await validatePath(filePath);
const content = await fs.readFile(validPath, "utf-8");
return `${filePath}:\n${content}\n`;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return `${filePath}: Error - ${errorMessage}`;
}
}),
);
}
export async function createDirectory(dirPath: string): Promise<void> {
const validPath = await validatePath(dirPath);
await fs.mkdir(validPath, { recursive: true });
}
export async function listDirectory(dirPath: string): Promise<string[]> {
const validPath = await validatePath(dirPath);
const entries = await fs.readdir(validPath, { withFileTypes: true });
return entries.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`);
}
export async function moveFile(sourcePath: string, destinationPath: string): Promise<void> {
const validSourcePath = await validatePath(sourcePath);
const validDestPath = await validatePath(destinationPath);
await fs.rename(validSourcePath, validDestPath);
}
export async function searchFiles(rootPath: string, pattern: string): Promise<string[]> {
const results: string[] = [];
async function search(currentPath: string) {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
try {
await validatePath(fullPath);
if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
results.push(fullPath);
}
if (entry.isDirectory()) {
await search(fullPath);
}
} catch (error) {
continue;
}
}
}
const validPath = await validatePath(rootPath);
await search(validPath);
return results;
}
export async function getFileInfo(filePath: string): Promise<Record<string, any>> {
const validPath = await validatePath(filePath);
const stats = await fs.stat(validPath);
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
permissions: stats.mode.toString(8).slice(-3),
};
}
export function listAllowedDirectories(): string[] {
return allowedDirectories;
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
type CallToolRequest,
} from "@modelcontextprotocol/sdk/types.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import { commandManager } from './command-manager.js';
import {
ExecuteCommandArgsSchema,
ReadOutputArgsSchema,
ForceTerminateArgsSchema,
ListSessionsArgsSchema,
KillProcessArgsSchema,
BlockCommandArgsSchema,
UnblockCommandArgsSchema,
ReadFileArgsSchema,
ReadMultipleFilesArgsSchema,
WriteFileArgsSchema,
CreateDirectoryArgsSchema,
ListDirectoryArgsSchema,
MoveFileArgsSchema,
SearchFilesArgsSchema,
GetFileInfoArgsSchema,
EditBlockArgsSchema,
} from './tools/schemas.js';
import { executeCommand, readOutput, forceTerminate, listSessions } from './tools/execute.js';
import { listProcesses, killProcess } from './tools/process.js';
import {
readFile,
readMultipleFiles,
writeFile,
createDirectory,
listDirectory,
moveFile,
searchFiles,
getFileInfo,
listAllowedDirectories,
} from './tools/filesystem.js';
import { parseEditBlock, performSearchReplace } from './tools/edit.js';
import { VERSION } from './version.js';
export const server = new Server(
{
name: "desktop-commander",
version: VERSION,
},
{
capabilities: {
tools: {},
},
},
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// Terminal tools
{
name: "execute_command",
description:
"Execute a terminal command with timeout. Command will continue running in background if it doesn't complete within timeout.",
inputSchema: zodToJsonSchema(ExecuteCommandArgsSchema),
},
{
name: "read_output",
description:
"Read new output from a running terminal session.",
inputSchema: zodToJsonSchema(ReadOutputArgsSchema),
},
{
name: "force_terminate",
description:
"Force terminate a running terminal session.",
inputSchema: zodToJsonSchema(ForceTerminateArgsSchema),
},
{
name: "list_sessions",
description:
"List all active terminal sessions.",
inputSchema: zodToJsonSchema(ListSessionsArgsSchema),
},
{
name: "list_processes",
description:
"List all running processes. Returns process information including PID, " +
"command name, CPU usage, and memory usage.",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "kill_process",
description:
"Terminate a running process by PID. Use with caution as this will " +
"forcefully terminate the specified process.",
inputSchema: zodToJsonSchema(KillProcessArgsSchema),
},
{
name: "block_command",
description:
"Add a command to the blacklist. Once blocked, the command cannot be executed until unblocked.",
inputSchema: zodToJsonSchema(BlockCommandArgsSchema),
},
{
name: "unblock_command",
description:
"Remove a command from the blacklist. Once unblocked, the command can be executed normally.",
inputSchema: zodToJsonSchema(UnblockCommandArgsSchema),
},
{
name: "list_blocked_commands",
description:
"List all currently blocked commands.",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
// Filesystem tools
{
name: "read_file",
description:
"Read the complete contents of a file from the file system. " +
"Handles various text encodings and provides detailed error messages " +
"if the file cannot be read. Only works within allowed directories.",
inputSchema: zodToJsonSchema(ReadFileArgsSchema),
},
{
name: "read_multiple_files",
description:
"Read the contents of multiple files simultaneously. " +
"Each file's content is returned with its path as a reference. " +
"Failed reads for individual files won't stop the entire operation. " +
"Only works within allowed directories.",
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema),
},
{
name: "write_file",
description:
"Completely replace file contents. Best for large changes (>20% of file) or when edit_block fails. " +
"Use with caution as it will overwrite existing files. Only works within allowed directories.",
inputSchema: zodToJsonSchema(WriteFileArgsSchema),
},
{
name: "create_directory",
description:
"Create a new directory or ensure a directory exists. Can create multiple " +
"nested directories in one operation. Only works within allowed directories.",
inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema),
},
{
name: "list_directory",
description:
"Get a detailed listing of all files and directories in a specified path. " +
"Results distinguish between files and directories with [FILE] and [DIR] prefixes. " +
"Only works within allowed directories.",
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema),
},
{
name: "move_file",
description:
"Move or rename files and directories. Can move files between directories " +
"and rename them in a single operation. Both source and destination must be " +
"within allowed directories.",
inputSchema: zodToJsonSchema(MoveFileArgsSchema),
},
{
name: "search_files",
description:
"Recursively search for files and directories matching a pattern. " +
"Searches through all subdirectories from the starting path. " +
"Only searches within allowed directories.",
inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
},
{
name: "get_file_info",
description:
"Retrieve detailed metadata about a file or directory including size, " +
"creation time, last modified time, permissions, and type. " +
"Only works within allowed directories.",
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema),
},
{
name: "list_allowed_directories",
description:
"Returns the list of directories that this server is allowed to access.",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "edit_block",
description:
"Apply surgical text replacements to files. Best for small changes (<20% of file size). " +
"Multiple blocks can be used for separate changes. Will verify changes after application. " +
"Format: filepath, then <<<<<<< SEARCH, content to find, =======, new content, >>>>>>> REPLACE.",
inputSchema: zodToJsonSchema(EditBlockArgsSchema),
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
try {
const { name, arguments: args } = request.params;
switch (name) {
// Terminal tools
case "execute_command": {
const parsed = ExecuteCommandArgsSchema.parse(args);
return executeCommand(parsed);
}
case "read_output": {
const parsed = ReadOutputArgsSchema.parse(args);
return readOutput(parsed);
}
case "force_terminate": {
const parsed = ForceTerminateArgsSchema.parse(args);
return forceTerminate(parsed);
}
case "list_sessions":
return listSessions();
case "list_processes":
return listProcesses();
case "kill_process": {
const parsed = KillProcessArgsSchema.parse(args);
return killProcess(parsed);
}
case "block_command": {
const parsed = BlockCommandArgsSchema.parse(args);
const blockResult = await commandManager.blockCommand(parsed.command);
return {
content: [{ type: "text", text: blockResult }],
};
}
case "unblock_command": {
const parsed = UnblockCommandArgsSchema.parse(args);
const unblockResult = await commandManager.unblockCommand(parsed.command);
return {
content: [{ type: "text", text: unblockResult }],
};
}
case "list_blocked_commands": {
const blockedCommands = await commandManager.listBlockedCommands();
return {
content: [{ type: "text", text: blockedCommands.join('\n') }],
};
}
// Filesystem tools
case "edit_block": {
const parsed = EditBlockArgsSchema.parse(args);
const { filePath, searchReplace } = await parseEditBlock(parsed.blockContent);
await performSearchReplace(filePath, searchReplace);
return {
content: [{ type: "text", text: `Successfully applied edit to ${filePath}` }],
};
}
case "read_file": {
const parsed = ReadFileArgsSchema.parse(args);
const content = await readFile(parsed.path);
return {
content: [{ type: "text", text: content }],
};
}
case "read_multiple_files": {
const parsed = ReadMultipleFilesArgsSchema.parse(args);
const results = await readMultipleFiles(parsed.paths);
return {
content: [{ type: "text", text: results.join("\n---\n") }],
};
}
case "write_file": {
const parsed = WriteFileArgsSchema.parse(args);
await writeFile(parsed.path, parsed.content);
return {
content: [{ type: "text", text: `Successfully wrote to ${parsed.path}` }],
};
}
case "create_directory": {
const parsed = CreateDirectoryArgsSchema.parse(args);
await createDirectory(parsed.path);
return {
content: [{ type: "text", text: `Successfully created directory ${parsed.path}` }],
};
}
case "list_directory": {
const parsed = ListDirectoryArgsSchema.parse(args);
const entries = await listDirectory(parsed.path);
return {
content: [{ type: "text", text: entries.join('\n') }],
};
}
case "move_file": {
const parsed = MoveFileArgsSchema.parse(args);
await moveFile(parsed.source, parsed.destination);
return {
content: [{ type: "text", text: `Successfully moved ${parsed.source} to ${parsed.destination}` }],
};
}
case "search_files": {
const parsed = SearchFilesArgsSchema.parse(args);
const results = await searchFiles(parsed.path, parsed.pattern);
return {
content: [{ type: "text", text: results.length > 0 ? results.join('\n') : "No matches found" }],
};
}
case "get_file_info": {
const parsed = GetFileInfoArgsSchema.parse(args);
const info = await getFileInfo(parsed.path);
return {
content: [{
type: "text",
text: Object.entries(info)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')
}],
};
}
case "list_allowed_directories": {
const directories = listAllowedDirectories();
return {
content: [{
type: "text",
text: `Allowed directories:\n${directories.join('\n')}`
}],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
```