@wonderwhy-er/desktop-commander 0.2.40 → 0.2.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/handlers/filesystem-handlers.js +6 -0
- package/dist/server.js +1 -0
- package/dist/tools/filesystem.js +48 -14
- package/dist/types.d.ts +1 -0
- package/dist/ui/file-preview/preview-runtime.js +93 -93
- package/dist/ui/file-preview/src/app.js +0 -5
- package/dist/ui/file-preview/src/directory-controller.js +9 -2
- package/dist/ui/file-preview/src/file-type-handlers.js +20 -9
- package/dist/ui/file-preview/src/payload-utils.js +10 -1
- package/dist/utils/feature-flags.d.ts +3 -0
- package/dist/utils/feature-flags.js +34 -5
- package/dist/utils/files/excel.js +26 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -335,9 +335,11 @@ Desktop Commander works with any MCP-compatible client. The standard JSON config
|
|
|
335
335
|
Add this to your client's MCP configuration file at the locations below:
|
|
336
336
|
|
|
337
337
|
<details>
|
|
338
|
-
<summary><b>Cursor</b></summary>
|
|
338
|
+
<summary><b>Cursor</b></summary><br>
|
|
339
339
|
|
|
340
|
-
[Install MCP Server
|
|
340
|
+
[](https://cursor.com/en-US/install-mcp?name=desktop-commander&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB3b25kZXJ3aHktZXIvZGVza3RvcC1jb21tYW5kZXJAbGF0ZXN0Il19)
|
|
341
|
+
|
|
342
|
+
[View MCP Server in Directory](https://cursor.directory/mcp/desktop-commander-mcp)
|
|
341
343
|
|
|
342
344
|
Or add manually to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in your project folder (project-specific).
|
|
343
345
|
|
|
@@ -99,6 +99,10 @@ export async function handleReadFile(args) {
|
|
|
99
99
|
fileName: path.basename(resolvedFilePath),
|
|
100
100
|
filePath: resolvedFilePath,
|
|
101
101
|
fileType: 'unsupported',
|
|
102
|
+
content: pdfContent
|
|
103
|
+
.filter((item) => item.type === "text")
|
|
104
|
+
.map((item) => item.text)
|
|
105
|
+
.join("\n"),
|
|
102
106
|
},
|
|
103
107
|
};
|
|
104
108
|
}
|
|
@@ -121,6 +125,7 @@ export async function handleReadFile(args) {
|
|
|
121
125
|
fileName: path.basename(resolvedFilePath),
|
|
122
126
|
filePath: resolvedFilePath,
|
|
123
127
|
fileType: 'image',
|
|
128
|
+
content: imageData,
|
|
124
129
|
imageData,
|
|
125
130
|
mimeType: fileResult.mimeType
|
|
126
131
|
}
|
|
@@ -140,6 +145,7 @@ export async function handleReadFile(args) {
|
|
|
140
145
|
fileName: path.basename(resolvedFilePath),
|
|
141
146
|
filePath: resolvedFilePath,
|
|
142
147
|
fileType,
|
|
148
|
+
content: textContent,
|
|
143
149
|
},
|
|
144
150
|
};
|
|
145
151
|
}
|
package/dist/server.js
CHANGED
|
@@ -445,6 +445,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
445
445
|
[FILE] src/tools/filesystem.ts
|
|
446
446
|
|
|
447
447
|
If a directory cannot be accessed, it will show [DENIED] instead.
|
|
448
|
+
If a path does not exist, it will show [NOT_FOUND] instead.
|
|
448
449
|
Only works within allowed directories.
|
|
449
450
|
|
|
450
451
|
${PATH_GUIDANCE}
|
package/dist/tools/filesystem.js
CHANGED
|
@@ -220,6 +220,38 @@ export async function validatePath(requestedPath) {
|
|
|
220
220
|
});
|
|
221
221
|
throw new Error(`Failed to resolve symlink for path: ${absoluteOriginal}. Error: ${err.message}`);
|
|
222
222
|
}
|
|
223
|
+
// SECURITY FIX: When the full path doesn't exist (e.g., writing a new file),
|
|
224
|
+
// resolve the parent directory to detect symlinks in the path chain.
|
|
225
|
+
// Without this, an attacker could create a symlink inside an allowed directory
|
|
226
|
+
// pointing to a restricted location, then write to a non-existent file through
|
|
227
|
+
// that symlink — bypassing the directory restriction check.
|
|
228
|
+
try {
|
|
229
|
+
const parentDir = path.dirname(absoluteOriginal);
|
|
230
|
+
const resolvedParent = await fs.realpath(parentDir, { encoding: 'utf8' });
|
|
231
|
+
const basename = path.basename(absoluteOriginal);
|
|
232
|
+
resolvedRealPath = path.join(resolvedParent, basename);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Parent also doesn't exist — walk up the tree to find
|
|
236
|
+
// the deepest existing ancestor and resolve it
|
|
237
|
+
let current = absoluteOriginal;
|
|
238
|
+
let remaining = [];
|
|
239
|
+
while (true) {
|
|
240
|
+
const parent = path.dirname(current);
|
|
241
|
+
if (parent === current)
|
|
242
|
+
break; // reached filesystem root
|
|
243
|
+
remaining.unshift(path.basename(current));
|
|
244
|
+
current = parent;
|
|
245
|
+
try {
|
|
246
|
+
const resolvedAncestor = await fs.realpath(current, { encoding: 'utf8' });
|
|
247
|
+
resolvedRealPath = path.join(resolvedAncestor, ...remaining);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// keep walking up
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
223
255
|
}
|
|
224
256
|
const pathForNextCheck = resolvedRealPath ?? absoluteOriginal;
|
|
225
257
|
// Check if path is allowed
|
|
@@ -230,25 +262,25 @@ export async function validatePath(requestedPath) {
|
|
|
230
262
|
});
|
|
231
263
|
throw new Error(`Path not allowed: ${requestedPath}. Must be within one of these directories: ${(await getAllowedDirs()).join(', ')}`);
|
|
232
264
|
}
|
|
265
|
+
// SECURITY: Always return the resolved path (with symlinks resolved) so that
|
|
266
|
+
// all subsequent file operations (read, write, mkdir, etc.) operate on the
|
|
267
|
+
// canonical target, not on a symlink that could point outside allowed directories.
|
|
268
|
+
// pathForNextCheck already holds resolvedRealPath ?? absoluteOriginal from above.
|
|
233
269
|
// Check if path exists
|
|
234
270
|
try {
|
|
235
271
|
// fs.stat() will automatically follow symlinks, so we get existence info
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (resolvedRealPath) {
|
|
239
|
-
return resolvedRealPath;
|
|
240
|
-
}
|
|
241
|
-
return absoluteOriginal;
|
|
272
|
+
await fs.stat(pathForNextCheck);
|
|
273
|
+
return pathForNextCheck;
|
|
242
274
|
}
|
|
243
275
|
catch (error) {
|
|
244
276
|
// Path doesn't exist - validate parent directories
|
|
245
|
-
if (await validateParentDirectories(
|
|
246
|
-
// Return the path if a valid parent exists
|
|
277
|
+
if (await validateParentDirectories(pathForNextCheck)) {
|
|
278
|
+
// Return the resolved path if a valid parent exists
|
|
247
279
|
// This will be used for folder creation and many other file operations
|
|
248
|
-
return
|
|
280
|
+
return pathForNextCheck;
|
|
249
281
|
}
|
|
250
|
-
// If no valid parent found, return the
|
|
251
|
-
return
|
|
282
|
+
// If no valid parent found, return the resolved path anyway
|
|
283
|
+
return pathForNextCheck;
|
|
252
284
|
}
|
|
253
285
|
};
|
|
254
286
|
// Execute with timeout
|
|
@@ -573,9 +605,11 @@ export async function listDirectory(dirPath, depth = 2) {
|
|
|
573
605
|
catch (error) {
|
|
574
606
|
const err = error;
|
|
575
607
|
const displayPath = relativePath || path.basename(currentPath);
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
608
|
+
// Distinguish "not found" from "permission denied" so AI and UI get accurate info.
|
|
609
|
+
if (err.code === 'ENOENT') {
|
|
610
|
+
results.push(`[NOT_FOUND] ${displayPath} — path does not exist`);
|
|
611
|
+
}
|
|
612
|
+
else if (err.code === 'EPERM' || err.code === 'EACCES' || err.code === 'ETIMEDOUT') {
|
|
579
613
|
results.push(`[DENIED] ${displayPath} — not accessible (permission denied, cloud-only file, or Full Disk Access not granted)`);
|
|
580
614
|
}
|
|
581
615
|
else {
|