@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
453 lines (452 loc) • 20.9 kB
JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListPromptsRequestSchema, } 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, SearchCodeArgsSchema, } 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 { searchTextInFiles } from './tools/search.js';
import { VERSION } from './version.js';
import { capture } from "./utils.js";
export const server = new Server({
name: "desktop-commander",
version: VERSION,
}, {
capabilities: {
tools: {},
resources: {}, // Add empty resources capability
prompts: {}, // Add empty prompts capability
},
});
// Add handler for resources/list method
server.setRequestHandler(ListResourcesRequestSchema, async () => {
// Return an empty list of resources
return {
resources: [],
};
});
// Add handler for prompts/list method
server.setRequestHandler(ListPromptsRequestSchema, async () => {
// Return an empty list of prompts
return {
prompts: [],
};
});
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. " +
"Reads UTF-8 text 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: "Finds files by name using a case-insensitive substring matching. " +
"Searches through all subdirectories from the starting path. " +
"Only searches within allowed directories.",
inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
},
{
name: "search_code",
description: "Search for text/code patterns within file contents using ripgrep. " +
"Fast and powerful search similar to VS Code search functionality. " +
"Supports regular expressions, file pattern filtering, and context lines. " +
"Only searches within allowed directories.",
inputSchema: zodToJsonSchema(SearchCodeArgsSchema),
},
{
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). " +
"Call repeatedly to change multiple blocks. Will verify changes after application. " +
"Format:\nfilepath\n<<<<<<< SEARCH\ncontent to find\n=======\nnew content\n>>>>>>> REPLACE",
inputSchema: zodToJsonSchema(EditBlockArgsSchema),
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
capture('server_call_tool');
switch (name) {
// Terminal tools
case "execute_command": {
capture('server_execute_command');
const parsed = ExecuteCommandArgsSchema.parse(args);
return executeCommand(parsed);
}
case "read_output": {
capture('server_read_output');
const parsed = ReadOutputArgsSchema.parse(args);
return readOutput(parsed);
}
case "force_terminate": {
capture('server_force_terminate');
const parsed = ForceTerminateArgsSchema.parse(args);
return forceTerminate(parsed);
}
case "list_sessions":
capture('server_list_sessions');
return listSessions();
case "list_processes":
capture('server_list_processes');
return listProcesses();
case "kill_process": {
capture('server_kill_process');
const parsed = KillProcessArgsSchema.parse(args);
return killProcess(parsed);
}
case "block_command": {
capture('server_block_command');
const parsed = BlockCommandArgsSchema.parse(args);
const blockResult = await commandManager.blockCommand(parsed.command);
return {
content: [{ type: "text", text: blockResult }],
};
}
case "unblock_command": {
capture('server_unblock_command');
const parsed = UnblockCommandArgsSchema.parse(args);
const unblockResult = await commandManager.unblockCommand(parsed.command);
return {
content: [{ type: "text", text: unblockResult }],
};
}
case "list_blocked_commands": {
capture('server_list_blocked_commands');
const blockedCommands = await commandManager.listBlockedCommands();
return {
content: [{ type: "text", text: blockedCommands.join('\n') }],
};
}
// Filesystem tools
case "edit_block": {
capture('server_edit_block');
try {
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}` }],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "read_file": {
capture('server_read_file');
try {
const parsed = ReadFileArgsSchema.parse(args);
const content = await readFile(parsed.path);
return {
content: [{ type: "text", text: content }],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "read_multiple_files": {
capture('server_read_multiple_files');
try {
const parsed = ReadMultipleFilesArgsSchema.parse(args);
const results = await readMultipleFiles(parsed.paths);
return {
content: [{ type: "text", text: results.join("\n---\n") }],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "write_file": {
capture('server_write_file');
try {
const parsed = WriteFileArgsSchema.parse(args);
await writeFile(parsed.path, parsed.content);
return {
content: [{ type: "text", text: `Successfully wrote to ${parsed.path}` }],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "create_directory": {
capture('server_create_directory');
try {
const parsed = CreateDirectoryArgsSchema.parse(args);
await createDirectory(parsed.path);
return {
content: [{ type: "text", text: `Successfully created directory ${parsed.path}` }],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "list_directory": {
capture('server_list_directory');
try {
const parsed = ListDirectoryArgsSchema.parse(args);
const entries = await listDirectory(parsed.path);
return {
content: [{ type: "text", text: entries.join('\n') }],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "move_file": {
capture('server_move_file');
try {
const parsed = MoveFileArgsSchema.parse(args);
await moveFile(parsed.source, parsed.destination);
return {
content: [{ type: "text", text: `Successfully moved ${parsed.source} to ${parsed.destination}` }],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "search_files": {
capture('server_search_files');
try {
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" }],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "search_code": {
capture('server_search_code');
let results = [];
try {
const parsed = SearchCodeArgsSchema.parse(args);
results = await searchTextInFiles({
rootPath: parsed.path,
pattern: parsed.pattern,
filePattern: parsed.filePattern,
ignoreCase: parsed.ignoreCase,
maxResults: parsed.maxResults,
includeHidden: parsed.includeHidden,
contextLines: parsed.contextLines,
});
if (results.length === 0) {
return {
content: [{ type: "text", text: "No matches found" }],
};
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
// Format the results in a VS Code-like format
let currentFile = "";
let formattedResults = "";
results.forEach(result => {
if (result.file !== currentFile) {
formattedResults += `\n${result.file}:\n`;
currentFile = result.file;
}
formattedResults += ` ${result.line}: ${result.match}\n`;
});
return {
content: [{ type: "text", text: formattedResults.trim() }],
};
}
case "get_file_info": {
capture('server_get_file_info');
try {
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')
}],
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
};
}
}
case "list_allowed_directories": {
capture('server_list_allowed_directories');
const directories = listAllowedDirectories();
return {
content: [{
type: "text",
text: `Allowed directories:\n${directories.join('\n')}`
}],
};
}
default:
capture('server_unknow_tool', {
name
});
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
capture('server_request_error', {
error: errorMessage
});
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
isError: true,
};
}
});