Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,9 @@ This makes it particularly useful for onboarding to new projects or reviewing un

Claude Desktop Commander has a sophisticated system for managing commands and operations that may take a while to complete:

1. The `execute_command` function returns after a timeout with initial output
1. The `start_process` function returns after a timeout with initial output
2. The command continues running in the background
3. You can use `read_output` with the PID to get new output as it becomes available
3. You can use `read_process_output` with the PID to get new output as it becomes available
4. You can use `force_terminate` to stop the command if needed

For search operations:
Expand Down Expand Up @@ -584,4 +584,4 @@ Jupyter notebooks and Claude Desktop Commander serve different purposes:
- Visual output for data visualization
- More structured for educational purposes

For data science or analysis projects, you might use both: Claude Desktop Commander for system tasks and code management, and Jupyter for interactive exploration and visualization.
For data science or analysis projects, you might use both: Claude Desktop Commander for system tasks and code management, and Jupyter for interactive exploration and visualization.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -890,8 +890,8 @@ By default, Desktop Commander shows helpful onboarding prompts to new users (tho
# Disable onboarding for this session
node dist/index.js --no-onboarding

# Or if using npm scripts
npm run start:no-onboarding
# Or when running the built server directly
node dist/index.js --no-onboarding

# For npx installations, modify your claude_desktop_config.json:
{
Expand Down Expand Up @@ -922,20 +922,20 @@ You can specify which shell to use for command execution:

```javascript
// Using default shell (bash or system default)
execute_command({ "command": "echo $SHELL" })
start_process({ "command": "echo $SHELL" })

// Using zsh specifically
execute_command({ "command": "echo $SHELL", "shell": "/bin/zsh" })
start_process({ "command": "echo $SHELL", "shell": "/bin/zsh" })

// Using bash specifically
execute_command({ "command": "echo $SHELL", "shell": "/bin/bash" })
start_process({ "command": "echo $SHELL", "shell": "/bin/bash" })
```

This allows you to use shell-specific features or maintain consistent environments across commands.

1. `execute_command` returns after timeout with initial output
1. `start_process` returns after timeout with initial output
2. Command continues in background
3. Use `read_output` with PID to get new output
3. Use `read_process_output` with PID to get new output
4. Use `force_terminate` to stop if needed

## Debugging
Expand Down
26 changes: 13 additions & 13 deletions install-docker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ function Show-Status {
Write-Info "Run reset and reinstall to fix this"
} else {
Write-Error "No volumes found - please run full installation"
Write-Info "Run: .\install-docker-clean.ps1"
Write-Info "Run: .\install-docker.ps1"
}
}function Reset-Installation {
Write-Header
Expand Down Expand Up @@ -785,7 +785,7 @@ function Show-Status {
}
Write-Host ""
Write-Info "To reinstall after reset:"
Write-Info "Run: .\install-docker-clean.ps1"
Write-Info "Run: .\install-docker.ps1"
} else {
Write-Info "Reset cancelled"
}
Expand All @@ -795,11 +795,11 @@ function Show-Help {
Write-Host "Desktop Commander Docker Installation (Enhanced)" -ForegroundColor Blue
Write-Host ""
Write-Host "Usage:"
Write-Host " .\install-docker-clean.ps1 - Interactive installation with folder selection"
Write-Host " .\install-docker-clean.ps1 -Status - Check installation status"
Write-Host " .\install-docker-clean.ps1 -Reset - Reset all data"
Write-Host " .\install-docker-clean.ps1 -VerboseOutput - Show detailed output"
Write-Host " .\install-docker-clean.ps1 -Help - Show this help"
Write-Host " .\install-docker.ps1 - Interactive installation with folder selection"
Write-Host " .\install-docker.ps1 -Status - Check installation status"
Write-Host " .\install-docker.ps1 -Reset - Reset all data"
Write-Host " .\install-docker.ps1 -VerboseOutput - Show detailed output"
Write-Host " .\install-docker.ps1 -Help - Show this help"
Write-Host ""
Write-Host "Features:"
Write-Host " - Interactive folder selection (like Mac version)"
Expand All @@ -809,8 +809,8 @@ function Show-Help {
Write-Host ""
Write-Host "Troubleshooting:"
Write-Host "If you broke the Docker container or need a fresh start:"
Write-Host " .\install-docker-clean.ps1 -Reset"
Write-Host " .\install-docker-clean.ps1"
Write-Host " .\install-docker.ps1 -Reset"
Write-Host " .\install-docker.ps1"
Write-Host ""
Write-Host "This will completely reset your persistent environment and reinstall everything fresh."
}
Expand Down Expand Up @@ -873,12 +873,12 @@ function Start-Installation {

Write-Host ""
Write-Info "To refresh/reset your persistent environment:"
Write-Info "- Run: .\install-docker-clean.ps1 -Reset"
Write-Info "- Run: .\install-docker.ps1 -Reset"
Write-Info "- This removes all installed packages and resets everything"
Write-Host ""
Write-Info "If you broke the Docker container or need a fresh start:"
Write-Info "- Run: .\install-docker-clean.ps1 -Reset"
Write-Info "- Then: .\install-docker-clean.ps1"
Write-Info "- Run: .\install-docker.ps1 -Reset"
Write-Info "- Then: .\install-docker.ps1"
Write-Info "- This will reset everything and reinstall from scratch"
Write-Host ""
Write-Info "Claude Desktop has been automatically restarted (if possible)"
Expand All @@ -897,4 +897,4 @@ function Start-Installation {
}

# Run installation
Start-Installation
Start-Installation
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions scripts/validate-tools-sync.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Validates that the tools listed in mcpb-bundle/manifest.json match
* Validates that the tools listed in manifest.template.json match
* the tools actually provided by the running MCP server
*
* This uses JSON-RPC to query the server directly, avoiding fragile regex parsing.
Expand All @@ -26,7 +26,9 @@ const colors = {
};

async function extractToolsFromManifest() {
const manifestPath = join(rootDir, 'mcpb-bundle', 'manifest.json');
// The generated MCPB bundle is intentionally gitignored. Use the tracked
// template so this validation works in a fresh checkout after `npm run build`.
const manifestPath = join(rootDir, 'manifest.template.json');
const content = await readFile(manifestPath, 'utf-8');
const manifest = JSON.parse(content);

Expand Down
6 changes: 3 additions & 3 deletions src/handlers/terminal-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import {
import { ServerResult } from '../types.js';

/**
* Handle start_process command (improved execute_command)
* Handle start_process command
*/
export async function handleStartProcess(args: unknown): Promise<ServerResult> {
const parsed = StartProcessArgsSchema.parse(args);
return startProcess(parsed);
}

/**
* Handle read_process_output command (improved read_output)
* Handle read_process_output command
*/
export async function handleReadProcessOutput(args: unknown): Promise<ServerResult> {
const parsed = ReadProcessOutputArgsSchema.parse(args);
Expand All @@ -52,4 +52,4 @@ export async function handleForceTerminate(args: unknown): Promise<ServerResult>
*/
export async function handleListSessions(): Promise<ServerResult> {
return listSessions();
}
}
4 changes: 2 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
Read contents from files and URLs.
Read PDF files and extract content as markdown and images.

Prefer this over 'execute_command' with cat/type for viewing files.
Prefer this over 'start_process' with cat/type for viewing files.

Supports partial file reading with:
- 'offset' (start line, default: 0)
Expand Down Expand Up @@ -568,7 +568,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
description: `
Get a detailed listing of all files and directories in a specified path.

Use this instead of 'execute_command' with ls/dir commands.
Use this instead of 'start_process' with ls/dir commands.
Results distinguish between files and directories with [FILE] and [DIR] prefixes.

Supports recursive listing with the 'depth' parameter (default: 2):
Expand Down
23 changes: 20 additions & 3 deletions src/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,14 +254,20 @@ export class TerminalManager {

// Spawn the process with appropriate arguments
const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions);
let spawnError: Error | undefined;
// `spawn` reports executable lookup failures asynchronously. Always attach
// a listener before checking pid so an invalid shell cannot crash the MCP server.
childProcess.on('error', (error) => {
spawnError = error;
});
let output = '';

// Ensure childProcess.pid is defined before proceeding
if (!childProcess.pid) {
// Return a consistent error object instead of throwing
return {
pid: -1, // Use -1 to indicate an error state
output: 'Error: Failed to get process ID. The command could not be executed.',
output: `Error: ${spawnError?.message ?? 'Failed to get process ID. The command could not be executed.'}`,
isBlocked: false
};
}
Expand Down Expand Up @@ -290,6 +296,7 @@ export class TerminalManager {
return new Promise((resolve) => {
let resolved = false;
let periodicCheck: NodeJS.Timeout | null = null;
let timeoutHandle: NodeJS.Timeout | null = null;

// Quick prompt patterns for immediate detection
const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;
Expand All @@ -298,6 +305,7 @@ export class TerminalManager {
if (resolved) return;
resolved = true;
if (periodicCheck) clearInterval(periodicCheck);
if (timeoutHandle) clearTimeout(timeoutHandle);

// Add timing info if requested
if (collectTiming) {
Expand Down Expand Up @@ -408,7 +416,7 @@ export class TerminalManager {
}, 100);

// Timeout fallback
setTimeout(() => {
timeoutHandle = setTimeout(() => {
session.isBlocked = true;
exitReason = 'timeout';
resolveOnce({
Expand All @@ -418,6 +426,15 @@ export class TerminalManager {
});
}, timeoutMs);

childProcess.on('error', (error) => {
exitReason = 'process_exit';
resolveOnce({
pid: childProcess.pid ?? -1,
output: `Error: ${error.message}`,
isBlocked: false
});
});

childProcess.on('exit', (code: any) => {
if (childProcess.pid) {
// Store completed session before removing active session
Expand Down Expand Up @@ -750,4 +767,4 @@ export class TerminalManager {
}
}

export const terminalManager = new TerminalManager();
export const terminalManager = new TerminalManager();
1 change: 1 addition & 0 deletions src/tools/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export async function getConfig() {
type: "text",
text: `Error getting configuration: ${error instanceof Error ? error.message : String(error)}\nUsing empty configuration.`
}],
isError: true,
};
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/tools/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,15 @@ export async function handleEditBlock(args: unknown): Promise<ServerResult> {
// Validate path and resolve handler once — used by both dispatch paths below
let validatedPath: string;
let handler: Awaited<ReturnType<typeof import('../utils/files/factory.js').getFileHandler>>;
let validatedOptions = parsed.options;
try {
validatedPath = await validatePath(parsed.file_path);
if (parsed.options?.outputPath) {
validatedOptions = {
...parsed.options,
outputPath: await validatePath(parsed.options.outputPath),
};
}
const { getFileHandler } = await import('../utils/files/factory.js');
handler = await getFileHandler(validatedPath);
} catch (error) {
Expand All @@ -435,7 +442,7 @@ export async function handleEditBlock(args: unknown): Promise<ServerResult> {
if (hasEditRange) {
try {
// parsed.range is guaranteed non-empty string by hasRange check above
await handler.editRange!(validatedPath!, parsed.range!, content, parsed.options);
await handler.editRange!(validatedPath!, parsed.range!, content, validatedOptions);
const resolvedRangePath = resolveAbsolutePath(parsed.file_path);
return {
content: [{
Expand Down
3 changes: 2 additions & 1 deletion src/tools/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ async function getAllowedDirs(): Promise<string[]> {

// Normalize all paths consistently
function normalizePath(p: string): string {
return path.normalize(expandHome(p)).toLowerCase();
const normalized = path.normalize(expandHome(p));
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
}

function expandHome(filepath: string): string {
Expand Down
4 changes: 2 additions & 2 deletions src/tools/improved-process-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async function executeNodeCode(code: string, timeout_ms: number = 30000): Promis
}

/**
* Start a new process (renamed from execute_command)
* Start a new process
* Includes early detection of process waiting for input
*/
export async function startProcess(args: unknown): Promise<ServerResult> {
Expand Down Expand Up @@ -722,4 +722,4 @@ export async function listSessions(): Promise<ServerResult> {
: allSessions.join('\n')
}],
};
}
}
Loading